diff --git a/public/css/va-register.css b/public/css/va-register.css index a90ec99..62fb0ce 100644 --- a/public/css/va-register.css +++ b/public/css/va-register.css @@ -1,222 +1,239 @@ * { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - } + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; +} - body { - background-color: #fff; - color: #333; - } +body { + background-color: #fff; + color: #333; +} - .container { - display: flex; - height: 100vh; - width: 100%; - } +.container { + display: flex; + height: 100vh; + width: 100%; +} - /* Sidebar Styles */ - .sidebar { - display: none; - flex-direction: column; - width: 344px; - background: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7); - padding: 24px; - border-radius: 14px; - margin: 20px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - } +/* Sidebar Styles */ +.sidebar { + display: none; + flex-direction: column; + width: 344px; + background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0); + padding: 24px; + border-radius: 14px; + margin: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} - .logo { - display: flex; - justify-content: center; - margin-bottom: 32px; - } - .logo-placeholder img{ - width: 100%; - height: auto; - max-width: 196px; - } +.logo { + display: flex; + justify-content: center; + margin-bottom: 32px; +} +.logo-placeholder img{ + width: 100%; + height: auto; + max-width: 196px; +} +.logo-placeholder h2 { + font-size: 32px; + font-weight: 700; + color: #000; + text-align: center; +} - .progress-steps { - display: flex; - flex-direction: column; - gap: 8px; - } +.form-title-sec { + font-size: 24px; + font-weight: 700; + color: #000; + text-align: center; + margin-bottom: 24px; +} - .step { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 12px; - border-radius: 12px; - border: 2px solid transparent; - cursor: pointer; - position: relative; - } +.progress-steps { + display: flex; + flex-direction: column; + gap: 8px; +} - .step.active { - border-color: #070065; - } +.step { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 12px; + border: 2px solid transparent; + cursor: pointer; + position: relative; +} - .step.completed .step-number { - background-color: #070065; - border-color: #070065; - color: white; - } +.step.active { + border-color: #000000; +} - .step-number { - width: 34px; - height: 34px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - border: 2px solid #a1a1aa; - font-weight: 600; - color: #a1a1aa; - transition: all 0.3s ease; - } +.step.completed .step-number { + background-color: #000000; + border-color: #000000; + color: white; +} - .step.active .step-number { - border-color: #000; - color: #000; - } +.step-number { + width: 34px; + height: 34px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #00000070; + font-weight: 600; + color: #000000b5; + transition: all 0.3s ease; +} - .step-text { - font-size: 14px; - color: #a1a1aa; - } +.step.active .step-number { + border-color: #000; + color: #000; +} - .step.active .step-text { - color: #000; - } +.step-text { + font-size: 14px; + color: #000000b5; +} - .step-connector { - height: 24px; - width: 2px; - background-color: #a1a1aa; - margin-left: 16px; - position: absolute; - top: 46px; - left: 12px; - } +.step.active .step-text { + color: #000; +} - .step.active .step-connector { - background-color: #000; - } +.step-connector { + height: 24px; + width: 2px; + background-color: #00000070; + margin-left: 16px; + position: absolute; + top: 46px; + left: 12px; +} - /* Main Content Styles */ - .main-content { - flex: 1; - display: flex; - flex-direction: column; - padding: 20px; - overflow-y: auto; - } +.step.active .step-connector { + background-color: #000; +} - .mobile-progress { - background-color: #f4f4f5; - padding: 16px; - border-radius: 12px; - margin-bottom: 20px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - overflow-x: auto; - } +/* Main Content Styles */ +.main-content { + flex: 1; + display: flex; + flex-direction: column; + padding: 20px; + overflow-y: auto; +} - .mobile-progress ol { - display: flex; - list-style: none; - gap: 12px; - min-width: max-content; - } +.mobile-progress { + background-color: #f4f4f5; + padding: 16px; + border-radius: 12px; + margin-bottom: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + overflow-x: auto; +} - .mobile-progress .step { - flex-shrink: 0; - } +.mobile-progress ol { + display: flex; + list-style: none; + gap: 12px; + min-width: max-content; +} - .form-container { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - padding: 40px 0; - } +.mobile-progress .step { + flex-shrink: 0; +} - .form-card { - width: 100%; - max-width: 450px; - background: white; - border: 1px solid #e5e7eb; - border-radius: 12px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); - padding: 24px; - } +.form-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 0; +} - .form-title { - text-align: center; - font-size: 20px; - font-weight: 600; - margin-bottom: 24px; - color: #000; - } +.form-card { + width: 100%; + max-width: 450px; + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + padding: 24px; +} - .form-step { - display: none; - } +.form-title { + text-align: center; + font-size: 20px; + font-weight: 600; + margin-bottom: 24px; + color: #000; +} - .form-step.active { - display: block; - } +.form-step { + display: none; +} - .form-group { - margin-bottom: 16px; - } +.form-step.active { + display: block; +} - .input-container { - position: relative; - background-color: #f9fafb; - border-radius: 6px; - overflow: hidden; - } +.form-group { + margin-bottom: 16px; +} - .input-label { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - font-size: 14px; - color: #6b7280; - transition: all 0.2s ease; - pointer-events: none; - } +.input-container { + position: relative; + background-color: #f9fafb; + border-radius: 6px; + overflow: hidden; + padding: 2px 0; +} - .input-field, .select-field { - width: 100%; - padding: 16px 12px; - border: none; - background: transparent; - outline: none; - font-size: 14px; - } +.input-label { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 14px; + color: #6b7280; + transition: all 0.2s ease; + pointer-events: none; +} - .select-field { +.input-field, .select-field { + width: 100%; + padding: 18px 12px 14px; + border: none; + background: transparent; + outline: none; + font-size: 14px; + border-radius: 6px; +} + +.select-field { appearance: none; background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; + border-radius: 6px; } - .input-field:focus + .input-label, - .input-field:not(:placeholder-shown) + .input-label, - .select-field:focus + .input-label, - .select-field:valid + .input-label { - top: 2px; - font-size: 12px; - transform: none; - } +.input-field:focus + .input-label, +.input-field:not(:placeholder-shown) + .input-label, +.select-field:focus + .input-label, +.select-field:valid + .input-label { + top: 2px; + font-size: 12px; + transform: none; +} .checkbox-group { display: flex; @@ -251,7 +268,7 @@ } .btn-primary { - background-color: #070065; + background-color: #000000; color: white; } @@ -268,67 +285,84 @@ background-color: #e5e7eb; } - .btn:disabled { - opacity: 0.6; - cursor: not-allowed; - } +.btn-outline { + background-color: transparent; + border: 1px solid #070065; + color: #070065; +} - .payment-option { - border: 1px solid #e5e7eb; - border-radius: 8px; - padding: 16px; - margin-bottom: 12px; - cursor: pointer; - } +.btn-outline:hover { + background-color: #070065; + color: white; +} - .payment-option.selected { - border-color: #070065; - background-color: #f0f9ff; - } - .payment-option h4 { - margin-bottom: 8px; - font-size: 16px; - } +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} - .payment-option p { - font-size: 14px; - color: #6b7280; - } +.btn-sm { + padding: 6px 12px; + font-size: 12px; +} - .payment-details { +.payment-option { + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; + margin-bottom: 12px; + cursor: pointer; +} + +.payment-option.selected { + border-color: #000000; + background-color: #f0f9ff; +} + +.payment-option h4 { + margin-bottom: 8px; + font-size: 16px; +} + +.payment-option p { + font-size: 14px; + color: #6b7280; +} + +.payment-details { display: none; - margin-top: 16px; + margin: 4px 0 16px; padding: 16px; border: 1px solid #e5e7eb; border-radius: 8px; - background-color: #f9fafb; + background-color: #bdbdbd; } .payment-details.active { display: block; } - .success-message { - text-align: center; - padding: 40px 20px; - } +.success-message { + text-align: center; + padding: 40px 20px; +} - .success-icon { - font-size: 48px; - color: #10b981; - margin-bottom: 16px; - } +.success-icon { + font-size: 48px; + color: #10b981; + margin-bottom: 16px; +} - .success-message h3 { - font-size: 24px; - margin-bottom: 8px; - } +.success-message h3 { + font-size: 24px; + margin-bottom: 8px; +} - .success-message p { - color: #6b7280; - margin-bottom: 24px; - } +.success-message p { + color: #6b7280; + margin-bottom: 24px; +} .location-loading { display: none; @@ -346,6 +380,85 @@ flex: 1; } +.profile-incomplete { + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1.5rem; + color: #856404; +} + +.password-strength { + height: 5px; + background: #e9ecef; + border-radius: 5px; + margin-top: 5px; + overflow: hidden; +} + +.password-strength-bar { + height: 100%; + width: 0; + border-radius: 5px; + transition: width 0.3s ease; +} + +.password-weak { + background: #dc3545; + width: 25%; +} + +.password-medium { + background: #ffc107; + width: 50%; +} + +.password-strong { + background: #28a745; + width: 100%; +} + +.password-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; +} + +.password-toggle { + background: none; + border: none; + color: #070065; + cursor: pointer; + font-size: 12px; +} + +.password-generate { + background: none; + border: none; + color: #070065; + cursor: pointer; + font-size: 12px; + text-decoration: underline; +} + +.form-text { + font-size: 12px; + color: #6b7280; + margin-top: 4px; +} + +.detect-location { + background: none; + border: none; + color: #070065; + cursor: pointer; + font-size: 12px; + text-decoration: underline; + margin-top: 5px; +} + /* Responsive styles */ @media (min-width: 1024px) { .sidebar { @@ -354,4 +467,9 @@ .mobile-progress { display: none; } + + .form-title-sec { + font-size: 40px; + margin-bottom: 40px; + } } \ No newline at end of file diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index 2fd6a43..deaae29 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -1,42 +1,121 @@ -// // Example: Basic client-side validation or UX improvements -// document.addEventListener("DOMContentLoaded", function () { -// const form = document.querySelector("form"); +// Email validation function +function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} -// form.addEventListener("submit", async (e) => { -// e.preventDefault(); +// Card type detection +function detectCardType(cardNumber) { + // Remove spaces and dashes + const cleanNumber = cardNumber.replace(/[\s-]/g, ''); -// handleSubmit(); -// }); -// }); + const cardPatterns = { + visa: /^4[0-9]{12}(?:[0-9]{3})?$/, + mastercard: /^5[1-5][0-9]{14}$/, + amex: /^3[47][0-9]{13}$/, + discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/, + diners: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/, + jcb: /^(?:2131|1800|35\d{3})\d{11}$/ + }; + + for (const [type, pattern] of Object.entries(cardPatterns)) { + if (pattern.test(cleanNumber)) { + return type; + } + } + return 'unknown'; +} -// function handleSubmit() { +// Card number validation (Luhn algorithm) +function validateCardNumber(cardNumber) { + const cleanNumber = cardNumber.replace(/[\s-]/g, ''); + + // Check if it's all digits and has proper length + if (!/^\d+$/.test(cleanNumber) || cleanNumber.length < 13 || cleanNumber.length > 19) { + return false; + } + + // Luhn algorithm + let sum = 0; + let isEven = false; + + for (let i = cleanNumber.length - 1; i >= 0; i--) { + let digit = parseInt(cleanNumber.charAt(i)); + + if (isEven) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + + sum += digit; + isEven = !isEven; + } + + return sum % 10 === 0; +} -// // Example: collect form data manually -// const form = document.querySelector("form"); -// const formData = new FormData(form); -// const payload = { userDetails: {} }; +// Expiry date validation +function validateExpiryDate(expiryDate) { + const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/; + if (!expiryRegex.test(expiryDate)) { + return false; + } + + const [month, year] = expiryDate.split('/'); + const expiry = new Date(2000 + parseInt(year), parseInt(month)); + const currentDate = new Date(); + const currentMonth = new Date(currentDate.getFullYear(), currentDate.getMonth()); + + return expiry >= currentMonth; +} -// formData.forEach((value, key) => { -// // handle nested names like userDetails[firstName] -// if (key.startsWith("userDetails[")) { -// const innerKey = key.match(/userDetails\[(.*?)\]/)[1]; -// payload.userDetails[innerKey] = value; -// } else { -// payload[key] = value; -// } -// }); -// // You could then send it via fetch: -// fetch("/api/v1/va/client/register", { -// method: "POST", -// body: JSON.stringify(payload), -// }) -// .then((res) => res.json()) -// .then((data) => { -// console.log("Server Response:", data); -// alert("Form submitted successfully!"); -// }) -// .catch((err) => console.error("Error:", err)); -// } +// CVV validation +function validateCVV(cvv, cardType) { + if (!/^\d+$/.test(cvv)) { + return false; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + return cvv.length === expectedLength; +} + +// ZIP code validation +function validateZipCode(zipCode) { + return /^\d{5}(-\d{4})?$/.test(zipCode); +} + +// Bank account validation +function validateBankAccount(accountNumber) { + return /^\d{8,17}$/.test(accountNumber.replace(/\s/g, '')); +} + +// Routing number validation (basic US routing number check) +function validateRoutingNumber(routingNumber) { + const cleanRouting = routingNumber.replace(/\s/g, ''); + + if (!/^\d{9}$/.test(cleanRouting)) { + return false; + } + + // Simple checksum validation + const digits = cleanRouting.split('').map(Number); + const sum = (3 * (digits[0] + digits[3] + digits[6])) + + (7 * (digits[1] + digits[4] + digits[7])) + + (digits[2] + digits[5] + digits[8]); + + return sum % 10 === 0; +} const formData = { userDetails: {} @@ -45,27 +124,27 @@ const formData = { // Current step tracking let currentStep = 1; -// const statesByCountry = { -// 'US': [ -// 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', -// 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', -// 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', -// 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', -// 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', -// 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', -// 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming' -// ], -// 'CA': [ -// 'Alberta', 'British Columbia', 'Manitoba', 'New Brunswick', 'Newfoundland and Labrador', -// 'Nova Scotia', 'Ontario', 'Prince Edward Island', 'Quebec', 'Saskatchewan' -// ], -// 'UK': [ -// 'England', 'Scotland', 'Wales', 'Northern Ireland' -// ], -// 'AU': [ -// 'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia' -// ] -// }; +const statesByCountry = { + 'US': [ + 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', + 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', + 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', + 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', + 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', + 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', + 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming' + ], + 'CA': [ + 'Alberta', 'British Columbia', 'Manitoba', 'New Brunswick', 'Newfoundland and Labrador', + 'Nova Scotia', 'Ontario', 'Prince Edward Island', 'Quebec', 'Saskatchewan' + ], + 'UK': [ + 'England', 'Scotland', 'Wales', 'Northern Ireland' + ], + 'AU': [ + 'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia' + ] +}; // Function to navigate to any step (for tab navigation) function goToStep(step) { @@ -115,8 +194,8 @@ function nextStep(step) { el.classList.add('completed'); const stepNumber = el.querySelector('.step-number'); stepNumber.textContent = '✓'; - stepNumber.style.backgroundColor = '#070065'; - stepNumber.style.borderColor = '#070065'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; stepNumber.style.color = 'white'; }); @@ -158,15 +237,173 @@ function validateStep(step) { const form = document.getElementById(getFormId(step)); if (form) { - const requiredFields = form.querySelectorAll('[required]'); - requiredFields.forEach(field => { - if (!field.value.trim()) { + if (step === 5) { + const selectedPayment = document.querySelector('.payment-option.selected'); + if (!selectedPayment) { isValid = false; - field.style.border = '1px solid #ef4444'; - } else { - field.style.border = ''; + alert('Please select a payment method'); + return isValid; } - }); + + const activePaymentDetails = document.querySelector('.payment-details.active'); + if (activePaymentDetails) { + const requiredFields = activePaymentDetails.querySelectorAll('[required]'); + + // Validate card payment + if (formData.userDetails.paymentMethod === 'card') { + // Card number validation + 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'); + } else { + document.getElementById('card-number').style.border = ''; + // Show card type + const cardType = detectCardType(cardNumber); + if (cardType !== 'unknown') { + // You can display card type to user if needed + console.log('Detected card type:', cardType); + } + } + + // Expiry date validation + 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 = ''; + } + + // CVV validation + const cvv = document.getElementById('security-code').value; + 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 = ''; + } + + // 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 = ''; + } + + } + // Validate bank payment + else if (formData.userDetails.paymentMethod === 'bank') { + // Account number validation + 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 = ''; + } + + // 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 = ''; + } + + // Basic validation for other bank fields + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } + } + } else if (step === 1) { + const email = document.getElementById('email').value; + if (!validateEmail(email)) { + isValid = false; + document.getElementById('email').style.border = '1px solid #ef4444'; + alert('Please enter a valid email address'); + } else { + document.getElementById('email').style.border = ''; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (field.id !== 'email' && !field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else if (field.id !== 'email') { + field.style.border = ''; + } + }); + } 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 = ''; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (field.id !== 'email' && !field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else if (field.id !== 'email') { + field.style.border = ''; + } + }); + } 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 = ''; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (field.id !== 'email' && !field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else if (field.id !== 'email') { + field.style.border = ''; + } + }); + } else { + // For other steps, validate all required fields normally + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } } return isValid; @@ -213,9 +450,6 @@ function saveStepData(step) { case 5: // Payment method already handled in selectPaymentOption break; - case 6: - formData.userDetails.paymentMethod = formData.payment?.type || ''; - break; } } @@ -228,6 +462,10 @@ function selectPaymentOption(type) { document.querySelectorAll('.payment-details').forEach(details => { details.classList.remove('active'); + // Remove required attributes from all payment fields + details.querySelectorAll('[required]').forEach(field => { + field.removeAttribute('required'); + }); }); // Add selected class to clicked option @@ -236,18 +474,23 @@ function selectPaymentOption(type) { if (type === 'card') { document.getElementById('card-details').classList.add('active'); formData.userDetails.paymentMethod = 'card'; - formData.userDetails.cardNumber = ''; - formData.userDetails.expiryDate = ''; - formData.userDetails.securityCode = ''; - formData.userDetails.cardZip = ''; + + // Add required attributes only to card fields + document.getElementById('card-number').setAttribute('required', 'true'); + document.getElementById('expiry-date').setAttribute('required', 'true'); + document.getElementById('security-code').setAttribute('required', 'true'); + document.getElementById('card-zip').setAttribute('required', 'true'); + } else if (type === 'bank') { document.getElementById('bank-details').classList.add('active'); formData.userDetails.paymentMethod = 'bank'; - formData.userDetails.bankName = ''; - formData.userDetails.accountNumber = ''; - formData.userDetails.routingNumber = ''; - formData.userDetails.accountType = ''; - formData.userDetails.accountHolderName = ''; + + // Add required attributes only to bank fields + document.getElementById('bank-name').setAttribute('required', 'true'); + document.getElementById('account-number').setAttribute('required', 'true'); + document.getElementById('routing-number').setAttribute('required', 'true'); + document.getElementById('account-type').setAttribute('required', 'true'); + document.getElementById('account-holder-name').setAttribute('required', 'true'); } } @@ -282,8 +525,9 @@ async function submitForm() { submitBtn.disabled = true; try { + // console.log('Submitting form data:', formData); // Send data to server using fetch - const response = await fetch('/api/v1/va/client/register', { + const response = await fetch('/api/v1/client/register', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -298,14 +542,18 @@ async function submitForm() { el.classList.add('completed'); const stepNumber = el.querySelector('.step-number'); stepNumber.textContent = '✓'; - stepNumber.style.backgroundColor = '#070065'; - stepNumber.style.borderColor = '#070065'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; stepNumber.style.color = 'white'; }); nextStep(6); } else { // Handle server errors - alert('There was an error submitting your registration. Please try again.'); + if (response.status === 409) { + alert('A user with this email already exists. Please use a different email.'); + } else { + alert('There was an error submitting your registration. Please try again.'); + } } } catch (error) { // Handle network errors @@ -318,51 +566,6 @@ async function submitForm() { } } -// Function to reset the form -function resetForm() { - // Reset all form fields - document.querySelectorAll('input').forEach(input => { - input.value = ''; - input.style.border = ''; - }); - - document.querySelectorAll('select').forEach(select => { - select.value = ''; - select.style.border = ''; - }); - - // Reset payment selection - document.querySelectorAll('.payment-option').forEach(option => { - option.classList.remove('selected'); - }); - - document.querySelectorAll('.payment-details').forEach(details => { - details.classList.remove('active'); - }); - - // Reset form data - formData.userDetails = {}; - - document.querySelectorAll('.step').forEach(step => { - step.classList.remove('completed', 'active'); - const stepNumber = step.querySelector('.step-number'); - if (stepNumber) { - stepNumber.textContent = step.getAttribute('data-step'); - stepNumber.style.backgroundColor = ''; - stepNumber.style.borderColor = '#a1a1aa'; - stepNumber.style.color = '#a1a1aa'; - } - }); - - // Go back to step 1 - document.getElementById('step-1').classList.add('active'); - document.querySelectorAll('.step[data-step="1"]').forEach(el => { - el.classList.add('active'); - }); - - currentStep = 1; -} - // Function to fetch states from API async function fetchStates(countryCode) { try { @@ -382,7 +585,7 @@ async function fetchStates(countryCode) { } // Function to populate states dropdown -function populateStates(countryCode, selectElementId) { +async function populateStates(countryCode, selectElementId) { const selectElement = document.getElementById(selectElementId); const states = statesByCountry[countryCode] || []; @@ -398,55 +601,131 @@ function populateStates(countryCode, selectElementId) { option.textContent = state; selectElement.appendChild(option); }); + // const selectElement = document.getElementById(selectElementId); + + // // Clear existing options except the first empty one + // while (selectElement.options.length > 1) { + // selectElement.remove(1); + // } + + // // Fetch states from API + // const states = await fetchStates(countryCode); + + // // Add states as options + // states.forEach(state => { + // const option = document.createElement('option'); + // option.value = state.code || state.name; + // option.textContent = state.name; + // selectElement.appendChild(option); + // }); } // Function to detect user location -function detectLocation() { +async function detectLocation() { // Show loading indicators document.getElementById('state-loading').style.display = 'block'; - // Using a free IP geolocation API - fetch('https://ipapi.co/json/') - .then(response => response.json()) - .then(data => { - // Hide loading indicators - document.getElementById('state-loading').style.display = 'none'; - - // Populate city and state based on detected location - document.getElementById('city').value = data.city || ''; - document.getElementById('zip').value = data.postal || ''; - - // Populate states dropdown for the detected country - populateStates(data.country_code, 'state'); - - // Set the detected state - if (data.region) { - document.getElementById('state').value = data.region; - } - - // Also populate billing states dropdown - populateStates(data.country_code, 'billing-state'); - }) - .catch(error => { - console.error('Error detecting location:', error); - - // Hide loading indicators - document.getElementById('state-loading').style.display = 'none'; - - // Fallback to US states if detection fails - populateStates('US', 'state'); - populateStates('US', 'billing-state'); - }); + try { + const response = await fetch('https://ipapi.co/json/'); + const data = await response.json(); + + // Hide loading indicators + document.getElementById('state-loading').style.display = 'none'; + + // Populate city and state based on detected location + document.getElementById('city').value = data.city || ''; + document.getElementById('zip').value = data.postal || ''; + + // Populate states dropdown for the detected country + await populateStates(data.country_code, 'state'); + + // Set the detected state + if (data.region) { + document.getElementById('state').value = data.region; + document.getElementById('state').dispatchEvent(new Event('change')); + } + + // Also populate billing states dropdown + await populateStates(data.country_code, 'billing-state'); + } catch (error) { + console.error('Error detecting location:', error); + + // Hide loading indicators + document.getElementById('state-loading').style.display = 'none'; + + // Fallback to US states if detection fails + await populateStates('US', 'state'); + await populateStates('US', 'billing-state'); + } } // Initialize form interactions document.addEventListener('DOMContentLoaded', function() { + // Real-time card number formatting and validation + const cardNumberInput = document.getElementById('card-number'); + if (cardNumberInput) { + cardNumberInput.addEventListener('input', function(e) { + // Format card number with spaces + let value = e.target.value.replace(/\s/g, ''); + let formattedValue = ''; + + for (let i = 0; i < value.length; i++) { + if (i > 0 && i % 4 === 0) { + formattedValue += ' '; + } + formattedValue += value[i]; + } + + e.target.value = formattedValue; + + // Real-time card type detection + const cardType = detectCardType(value); + if (cardType !== 'unknown') { + // You can add visual feedback for card type here + console.log('Card type:', cardType); + } + }); + } + + // Real-time expiry date formatting + const expiryInput = document.getElementById('expiry-date'); + if (expiryInput) { + expiryInput.addEventListener('input', function(e) { + let value = e.target.value.replace(/\D/g, ''); + if (value.length >= 2) { + value = value.substring(0, 2) + '/' + value.substring(2, 4); + } + e.target.value = value; + }); + } + + // Real-time CVV validation + const cvvInput = document.getElementById('security-code'); + if (cvvInput) { + cvvInput.addEventListener('input', function(e) { + e.target.value = e.target.value.replace(/\D/g, '').substring(0, 4); + }); + } + + // Real-time email validation + const emailInputs = document.querySelectorAll('input[type="email"]'); + emailInputs.forEach(emailInput => { + if (emailInput) { + emailInput.addEventListener('blur', function(e) { + if (e.target.value && !validateEmail(e.target.value)) { + e.target.style.border = '1px solid #ef4444'; + } else { + e.target.style.border = ''; + } + }); + } + }); // Add input event listeners to handle label animation document.querySelectorAll('.input-field').forEach(input => { input.addEventListener('input', function() { if (this.value) { - this.nextElementSibling.style.top = '2px'; + this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.transform = 'none'; } else { @@ -457,7 +736,7 @@ document.addEventListener('DOMContentLoaded', function() { }); input.addEventListener('focus', function() { - this.nextElementSibling.style.top = '2px'; + this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.transform = 'none'; }); @@ -475,7 +754,7 @@ document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.select-field').forEach(select => { select.addEventListener('change', function() { if (this.value) { - this.nextElementSibling.style.top = '2px'; + this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.transform = 'none'; } else { @@ -486,7 +765,7 @@ document.addEventListener('DOMContentLoaded', function() { }); select.addEventListener('focus', function() { - this.nextElementSibling.style.top = '2px'; + this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.transform = 'none'; }); @@ -522,7 +801,7 @@ document.addEventListener('DOMContentLoaded', function() { // Handle use account email checkbox document.getElementById('use-account-email').addEventListener('change', function() { if (this.checked) { - document.getElementById('billing-email').value = formData.account.email || ''; + document.getElementById('billing-email').value = formData.userDetails.email || ''; document.getElementById('billing-email').dispatchEvent(new Event('input')); } }); diff --git a/public/js/va-register.js b/public/js/va-register.js index 2fdb4fb..2a1fe50 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -1,46 +1,733 @@ -// Example: Basic client-side validation or UX improvements -document.addEventListener("DOMContentLoaded", function () { - const form = document.querySelector("form"); - form.addEventListener("submit", async (e) => { - e.preventDefault(); - const password = document.getElementById("password").value; - const cPassword = document.getElementById("cPassword").value; +// Form data object + const formData = { + userDetails: {} + }; - if (password !== cPassword) { - alert("Passwords do not match!"); - return; - } - - handleSubmit(); - }); -}); + // Current step tracking + let currentStep = 1; -function handleSubmit() { + // Countries and states data + const countries = [ + { code: 'US', name: 'United States' }, + { code: 'CA', name: 'Canada' }, + { code: 'GB', name: 'United Kingdom' }, + { code: 'AU', name: 'Australia' }, + { code: 'IN', name: 'India' }, + { code: 'DE', name: 'Germany' }, + { code: 'FR', name: 'France' }, + { code: 'BR', name: 'Brazil' }, + { code: 'JP', name: 'Japan' }, + { code: 'CN', name: 'China' } + ]; - // Example: collect form data manually - const form = document.querySelector("form"); - const formData = new FormData(form); - const payload = { userDetails: {} }; + const states = { + 'US': [ + { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' }, + { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' }, + { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' }, + { code: 'MI', name: 'Michigan' } + ], + 'CA': [ + { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' }, + { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' } + ], + 'GB': [ + { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' }, + { code: 'NIR', name: 'Northern Ireland' } + ], + 'AU': [ + { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' }, + { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' } + ], + 'IN': [ + { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' }, + { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' } + ], + 'DE': [ + { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' }, + { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' } + ], + 'FR': [ + { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' }, + { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' }, + { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' } + ], + 'BR': [ + { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' }, + { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' } + ], + 'JP': [ + { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' }, + { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' } + ], + 'CN': [ + { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' }, + { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' } + ] + }; - formData.forEach((value, key) => { - // handle nested names like userDetails[firstName] - if (key.startsWith("userDetails[")) { - const innerKey = key.match(/userDetails\[(.*?)\]/)[1]; - payload.userDetails[innerKey] = value; - } else { - payload[key] = value; - } + // Initialize the form + document.addEventListener('DOMContentLoaded', function() { + // Populate country dropdown + const countrySelect = document.getElementById('country'); + countrySelect.innerHTML = ''; + + countries.forEach(country => { + const option = document.createElement('option'); + option.value = country.code; + option.textContent = country.name; + countrySelect.appendChild(option); + }); + + // Add event listener for country change + countrySelect.addEventListener('change', function() { + const countryCode = this.value; + const stateSelect = document.getElementById('state'); + + // Show loading + document.getElementById('state-loading').style.display = 'block'; + + // Clear current states + stateSelect.innerHTML = ''; + + // Simulate API delay + setTimeout(() => { + if (countryCode && states[countryCode]) { + states[countryCode].forEach(state => { + const option = document.createElement('option'); + option.value = state.code; + option.textContent = state.name; + stateSelect.appendChild(option); + }); + } + + // Hide loading + document.getElementById('state-loading').style.display = 'none'; + }, 500); + }); + + // Password strength indicator + document.getElementById('password').addEventListener('input', function() { + updatePasswordStrength(this.value); + }); + + // Add input event listeners to handle label animation + document.querySelectorAll('.input-field').forEach(input => { + input.addEventListener('input', function() { + if (this.value) { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + } else { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + + input.addEventListener('focus', function() { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + }); + + input.addEventListener('blur', function() { + if (!this.value) { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + }); + + // Handle select field label animation + document.querySelectorAll('.select-field').forEach(select => { + select.addEventListener('change', function() { + if (this.value) { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + } else { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + + select.addEventListener('focus', function() { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + }); + + select.addEventListener('blur', function() { + if (!this.value) { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + }); + + // Step navigation for desktop + document.querySelectorAll('.step').forEach(step => { + step.addEventListener('click', function() { + const stepNum = parseInt(this.getAttribute('data-step')); + if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + goToStep(stepNum); + } + }); + }); + + // Step navigation for mobile + document.querySelectorAll('.mobile-step').forEach(step => { + step.addEventListener('click', function() { + const stepNum = parseInt(this.getAttribute('data-step')); + if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + goToStep(stepNum); + } + }); + }); }); - // You could then send it via fetch: - fetch("/api/v1/va/register", { - method: "POST", - body: JSON.stringify(payload), - }) - .then((res) => res.json()) - .then((data) => { - console.log("Server Response:", data); - alert("Form submitted successfully!"); - }) - .catch((err) => console.error("Error:", err)); -} + + // Function to navigate to any step (for tab navigation) + function goToStep(step) { + // Only allow navigation to completed steps or the next step + if (step > currentStep && step > getMaxCompletedStep() + 1) { + return; + } + + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + // Show target step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + currentStep = step; + } + + // Function to get the maximum completed step + function getMaxCompletedStep() { + const completedSteps = document.querySelectorAll('.step.completed'); + let maxStep = 0; + completedSteps.forEach(step => { + const stepNum = parseInt(step.getAttribute('data-step')); + if (stepNum > maxStep) maxStep = stepNum; + }); + return maxStep; + } + + // Function to navigate to next step + function nextStep(step) { + // Validate current step before proceeding + if (!validateStep(currentStep)) { + return; + } + + // Save current step data + saveStepData(currentStep); + + // Mark current step as completed + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + // Show next step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + currentStep = step; + } + + // Function to navigate to previous step + function prevStep(step) { + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + // Show previous step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + currentStep = step; + } + + // Function to validate current step + function validateStep(step) { + let isValid = true; + const form = document.getElementById(getFormId(step)); + + if (form) { + // For account setup step + if (step === 1) { + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + // Email validation + if (!validateEmail(email)) { + alert('Please enter a valid email address'); + isValid = false; + } + + // Password validation + if (password.length < 8) { + alert('Password must be at least 8 characters long'); + isValid = false; + } + + if (password !== confirmPassword) { + document.getElementById('password-match-error').style.display = 'block'; + isValid = false; + } else { + document.getElementById('password-match-error').style.display = 'none'; + } + } + + // For personal information step + if (step === 2) { + const firstName = document.getElementById('firstName').value; + if (!firstName) { + document.getElementById('profile-incomplete').style.display = 'block'; + isValid = false; + } else { + document.getElementById('profile-incomplete').style.display = 'none'; + } + + // Age validation + const age = document.getElementById('age').value; + if (age && (age < 18 || age > 100)) { + alert('You must be 18 years or older to register'); + isValid = false; + } + + // Country and state validation + const country = document.getElementById('country').value; + const state = document.getElementById('state').value; + + if (!country) { + alert('Please select your country'); + isValid = false; + } + + if (!state) { + alert('Please select your state/region'); + isValid = false; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } + + // For payment step + if (step === 4) { + const selectedPayment = document.querySelector('.payment-option.selected'); + if (!selectedPayment) { + alert('Please select a payment method'); + 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'); + isValid = false; + } + } else if (paymentMethod === 'veem') { + const veemAccount = document.getElementById('veemAccount').value; + if (!veemAccount) { + alert('Please enter your Veem account number'); + isValid = false; + } + } + } + } + + // For other steps + if (step !== 1 && step !== 2 && step !== 4) { + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } + } + + return isValid; + } + + // Email validation function + function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + // Function to get form ID based on step + function getFormId(step) { + switch(step) { + case 1: return 'account-form'; + case 2: return 'personal-form'; + case 3: return 'contact-form'; + case 4: return 'payment-form'; + default: return ''; + } + } + + // Function to save step data to formData object + function saveStepData(step) { + switch(step) { + case 1: + formData.userDetails.email = document.getElementById('email').value; + formData.userDetails.password = document.getElementById('password').value; + break; + case 2: + formData.userDetails.firstName = document.getElementById('firstName').value; + formData.userDetails.lastName = document.getElementById('lastName').value; + formData.userDetails.age = document.getElementById('age').value; + formData.userDetails.dob = document.getElementById('dob').value; + formData.userDetails.address = document.getElementById('address').value; + formData.userDetails.city = document.getElementById('city').value; + formData.userDetails.state = document.getElementById('state').value; + formData.userDetails.country = document.getElementById('country').value; + break; + case 3: + formData.userDetails.phone = document.getElementById('phone').value; + formData.userDetails.skype = document.getElementById('skype').value; + break; + case 4: + const selectedPayment = document.querySelector('.payment-option.selected'); + if (selectedPayment) { + const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; + formData.userDetails.paymentMethod = paymentMethod; + + if (paymentMethod === 'payoneer') { + formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value; + } else if (paymentMethod === 'veem') { + formData.userDetails.veemAccount = document.getElementById('veemAccount').value; + } + } + break; + } + } + + // Function to select payment method + function selectPaymentMethod(method) { + // Remove selected class from all options + document.querySelectorAll('.payment-option').forEach(option => { + option.classList.remove('selected'); + }); + + // Hide all payment details + document.querySelectorAll('.payment-details').forEach(details => { + details.style.display = 'none'; + }); + + // Add selected class to clicked option + event.currentTarget.classList.add('selected'); + + // Show appropriate payment details + if (method === 'payoneer') { + document.getElementById('payoneer-details').style.display = 'block'; + } else if (method === 'veem') { + document.getElementById('veem-details').style.display = 'block'; + } + } + + // Function to generate a strong password + function generatePassword() { + const length = 12; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"; + let password = ""; + + // Ensure at least one of each required character type + password += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)]; + password += "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)]; + password += "0123456789"[Math.floor(Math.random() * 10)]; + password += "!@#$%^&*"[Math.floor(Math.random() * 8)]; + + // Fill the rest randomly + for (let i = password.length; i < length; i++) { + password += charset.charAt(Math.floor(Math.random() * charset.length)); + } + + // Shuffle the password + password = password.split('').sort(() => 0.5 - Math.random()).join(''); + + // Set the password field + document.getElementById('password').value = password; + document.getElementById('confirmPassword').value = password; + + // Trigger input events to update UI + document.getElementById('password').dispatchEvent(new Event('input')); + document.getElementById('confirmPassword').dispatchEvent(new Event('input')); + + // Update password strength indicator + updatePasswordStrength(password); + } + + // Function to toggle password visibility + function togglePasswordVisibility() { + const passwordField = document.getElementById('password'); + const confirmField = document.getElementById('confirmPassword'); + const toggleButton = document.querySelector('.password-toggle'); + + if (passwordField.type === 'password') { + passwordField.type = 'text'; + confirmField.type = 'text'; + toggleButton.textContent = 'Hide Password'; + } else { + passwordField.type = 'password'; + confirmField.type = 'password'; + toggleButton.textContent = 'Show Password'; + } + } + + // Function to update password strength indicator + function updatePasswordStrength(password) { + const strengthBar = document.getElementById('password-strength-bar'); + + // Reset classes + strengthBar.className = 'password-strength-bar'; + + if (password.length === 0) { + strengthBar.style.width = '0'; + } else if (password.length < 6) { + strengthBar.classList.add('password-weak'); + } else if (password.length < 10) { + strengthBar.classList.add('password-medium'); + } else { + // Check for complexity + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasNumbers = /\d/.test(password); + const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password); + + const complexityScore = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecial].filter(Boolean).length; + + if (complexityScore >= 3) { + strengthBar.classList.add('password-strong'); + } else { + strengthBar.classList.add('password-medium'); + } + } + } + + // Function to submit the form + async function submitForm() { + // Validate current step + if (!validateStep(currentStep)) { + return; + } + + // Save current step data + saveStepData(currentStep); + + // Show loading state + const submitBtn = document.querySelector('#payment-form .btn-primary'); + const originalText = submitBtn.textContent; + submitBtn.textContent = 'Processing...'; + submitBtn.disabled = true; + + try { + // Send data to server using fetch + // console.log('Submitting form data:', formData); + const response = await fetch('/api/v1/va/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + // Success - move to success step + // Mark current step as completed + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + nextStep(5); + } else { + // Handle server errors + alert('There was an error submitting your registration. Please try again.'); + } + } catch (error) { + // Handle network errors + console.error('Error:', error); + alert('There was a network error. Please check your connection and try again.'); + } finally { + // Restore button state + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } + } + + + // Initialize form interactions + // document.addEventListener('DOMContentLoaded', function() { + // // Password strength indicator + // document.getElementById('password').addEventListener('input', function() { + // updatePasswordStrength(this.value); + // }); + + // // Add input event listeners to handle label animation + // document.querySelectorAll('.input-field').forEach(input => { + // input.addEventListener('input', function() { + // if (this.value) { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // } else { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + + // input.addEventListener('focus', function() { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // }); + + // input.addEventListener('blur', function() { + // if (!this.value) { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + // }); + + // // Handle select field label animation + // document.querySelectorAll('.select-field').forEach(select => { + // select.addEventListener('change', function() { + // if (this.value) { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // } else { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + + // select.addEventListener('focus', function() { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // }); + + // select.addEventListener('blur', function() { + // if (!this.value) { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + // }); + + // // Step navigation for desktop + // document.querySelectorAll('.step').forEach(step => { + // step.addEventListener('click', function() { + // const stepNum = parseInt(this.getAttribute('data-step')); + // if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + // goToStep(stepNum); + // } + // }); + // }); + + // // Step navigation for mobile + // document.querySelectorAll('.mobile-step').forEach(step => { + // step.addEventListener('click', function() { + // const stepNum = parseInt(this.getAttribute('data-step')); + // if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + // goToStep(stepNum); + // } + // }); + // }); + // }); \ No newline at end of file diff --git a/src/controllers/client/clientRefreshToken.controller.js b/src/controllers/client/clientRefreshToken.controller.js new file mode 100644 index 0000000..f705fbe --- /dev/null +++ b/src/controllers/client/clientRefreshToken.controller.js @@ -0,0 +1,65 @@ +import jwt from "jsonwebtoken"; +import { ClientModel } from "../../models/vaModel.js"; + +// Refresh Token VA +async function client_refresh_token_callback(req, res) { + try { + const { refreshToken } = req.body || {}; + + if (!refreshToken) { + return res.status(400).json({ + success: false, + message: "Refresh token missing", + }); + } + + // 🔍 Verify refresh token + jwt.verify( + refreshToken, + process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken", + async (err, decoded) => { + if (err) { + return res.status(403).json({ + success: false, + message: "Invalid or expired refresh token", + }); + } + + // Check if user still exists + const user = await ClientModel.findById(decoded.id); + if (!user) { + return res.status(404).json({ + success: false, + message: "User no longer exists", + }); + } + + // Generate a new access token + const newAccessToken = jwt.sign( + { + id: user._id, + email: user.userDetails.email, + name: user.userDetails.firstName, + }, + process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerAccessKey", + { expiresIn: "24h" } + ); + + await ClientModel.updateOne({ "userDetails.email": user.userDetails.email }, { + $set: { accessToken: newAccessToken } + }) + + return res.status(200).json({ + success: true, + message: "New access token generated", + accessToken: newAccessToken, + }); + } + ); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { client_refresh_token_callback }; \ No newline at end of file diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js new file mode 100644 index 0000000..f34de5d --- /dev/null +++ b/src/controllers/client/vaClient.controller.js @@ -0,0 +1,76 @@ +import VaClientRepository from "../../repository/vaClient.repository.js"; +import VaClientService from "../../services/vaClient.service.js"; +import logger from "../../config/logger.js"; + +const vaClientService = new VaClientService(new VaClientRepository()); + + +async function va_register_client(req, res) { + logger.info("ClientController - Register Client Hit ..."); + + try { + const payload = req.body || {}; + const { userDetails } = payload; + + if (!userDetails || !userDetails.email || !userDetails.name) { + return res.status(400).json({ + success: false, + message: "Missing required fields: name and email are required" + }); + } + + const created = await vaClientService.registerClient(payload); + + logger.info(`ClientController - Client registered successfully: ${userDetails.email}`); + return res.status(201).json({ + success: true, + data: created, + message: "Client registered successfully" + }); + } catch (error) { + logger.error(`ClientController - Register error: ${error.message}`); + + if (error.message === "User already exists with this email") { + return res.status(409).json({ + success: false, + message: error.message + }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + +// Additional controller methods can be added here +async function get_client_by_email(req, res) { + try { + const { email } = req.params; + const client = await vaClientService.getClientByEmail(email); + + if (!client) { + return res.status(404).json({ + success: false, + message: "Client not found" + }); + } + + return res.status(200).json({ + success: true, + data: client + }); + } catch (error) { + logger.error(`ClientController - Get client error: ${error.message}`); + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + +export { + va_register_client, + get_client_by_email +}; \ No newline at end of file diff --git a/src/controllers/client/vaClientLogin.controller.js b/src/controllers/client/vaClientLogin.controller.js new file mode 100644 index 0000000..384c132 --- /dev/null +++ b/src/controllers/client/vaClientLogin.controller.js @@ -0,0 +1,79 @@ +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { ClientModel } from "../../models/vaModel.js"; + +// Login VA +async function va_login_client(req, res) { + try { + // const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body || {}; + const { userDetails } = payload; + + if (!userDetails || !userDetails.email || !userDetails.password) { + return res.status(400).json({ success: false, message: "Please provide email and password" }); + } + + // Find user by email + const user = await ClientModel.findOne({ "userDetails.email": userDetails.email }); + if (!user) { + return res.status(404).json({ success: false, message: "Invalid credentials" }); + } + + // const saltRounds = 10; + // const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds); + + // Compare password with stored hash + const isMatch = await bcrypt.compare( + userDetails.password, + user.userDetails.password + ); + + if (!isMatch) { + return res.status(401).json({ success: false, message: "Invalid password" }); + } + + // Generate JWT accessToken with 24h expiry + const accessToken = jwt.sign( + { + id: user._id, + email: user.userDetails.email, + name: user.userDetails.firstName, + }, + process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey", + { expiresIn: "24h" } + ); + + // Generate JWT refreshToken with 24h expiry + const refreshToken = jwt.sign( + { + id: user._id, + email: user.userDetails.email, + name: user.userDetails.firstName, + }, + process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken", + { expiresIn: "30d" } + ); + + await ClientModel.updateOne({ "userDetails.email": userDetails.email }, { + $set: { accessToken, refreshToken } + }) + + // Return accessToken and user info + return res.status(200).json({ + success: true, + message: "Login Successfull", + accessToken, + refreshToken, + user: { + id: user._id, + name: user.userDetails.firstName, + email: user.userDetails.email, + }, + }); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_login_client }; \ No newline at end of file diff --git a/src/controllers/client/vaClientPasswordReset.controllers.js b/src/controllers/client/vaClientPasswordReset.controllers.js new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/va/vaClient/vaClient.controller.js b/src/controllers/va/vaClient/vaClient.controller.js deleted file mode 100644 index b29fdec..0000000 --- a/src/controllers/va/vaClient/vaClient.controller.js +++ /dev/null @@ -1,43 +0,0 @@ -import { ClientModel } from "../../../models/vaModel.js"; - -// Register a new VA -async function va_register_client(req, res) { - try { - const payload = req.body ? JSON.parse(req.body) : {}; - const { userDetails } = payload; - - if (!userDetails || !userDetails.email || !userDetails.name) { - return res.status(400).json({ success: false, message: "Missing required fields" }); - } - - // ✅ Check if user already exists by email - const existingUser = await ClientModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() }); - if (existingUser) { - return res.status(409).json({ success: false, message: "User already exists with this email" }); - } - - // sanitize/construct allowed fields - const toCreate = { - userDetails: { - name: userDetails.name, - email: userDetails.email, - dob: userDetails.dob || null, - bankName: userDetails.bankName || null, - accountHolderName: userDetails.accountHolderName || null, - accountNumber: userDetails.accountNumber || null, - routingNumber: userDetails.routingNumber || null, - }, - vaIds: payload.vaIds || [], - projectIds: payload.projectIds || [], - }; - - const created = await ClientModel.create(toCreate); - - return res.status(201).json({ success: true, data: created }); - } catch (error) { - console.error("va_register_client error:", error); - return res.status(500).json({ success: false, message: "Internal Server Error" }); - } -} - -export { va_register_client }; \ No newline at end of file diff --git a/src/controllers/va/vaPasswordReset.controllers.js b/src/controllers/va/vaPasswordReset.controllers.js new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js index 9354858..61352fc 100644 --- a/src/controllers/va/vaProfile/vaRegister.controller.js +++ b/src/controllers/va/vaProfile/vaRegister.controller.js @@ -4,17 +4,13 @@ import { VAModel } from "../../../models/vaModel.js"; // Register a new VA async function va_register(req, res) { try { - const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body|| {}; const { userDetails } = payload; if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) { return res.status(400).json({ success: false, message: "Missing required fields" }); } - if (userDetails.password !== userDetails.cPassword) { - return res.status(400).json({ success: false, message: "password not matching" }); - } - // ✅ Check if user already exists by email const existingUser = await VAModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() }); if (existingUser) { @@ -33,11 +29,15 @@ async function va_register(req, res) { dob: userDetails.dob || null, age: userDetails.age || null, address: userDetails.address || null, + city: userDetails.city || null, + state: userDetails.state || null, + country: userDetails.country || null, password: hashedPassword, - bankName: userDetails.bankName || null, - accountHolderName: userDetails.accountHolderName || null, - accountNumber: userDetails.accountNumber || null, - routingNumber: userDetails.routingNumber || null, + paymentMethod: userDetails.paymentMethod || null, + payoneerAccount: userDetails.payoneerAccount || null, + veemAccount: userDetails.veemAccount || null, + phone: userDetails.phone || null, + skype: userDetails.skype || null, }, tracking: payload.tracking || undefined, clientIds: payload.clientIds || [], diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 3b84daa..40dc157 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -25,19 +25,31 @@ const userDetailsSchema = new Schema({ address: { type: String, }, + city: { + type: String, + }, + state: { + type: String, + }, + country: { + type: String, + }, password: { type: String, }, - bankName: { + paymentMethod: { type: String, }, - accountHolderName: { + payoneerAccount: { type: String, }, - accountNumber: { + veemAccount: { type: String, }, - routingNumber: { + phone: { + type: String, + }, + skype: { type: String, }, }); @@ -61,6 +73,9 @@ const projectSchema = new Schema({ type: String, required: true, }, + hourlyRate: { + type: String, + }, totalLoggedHours: { type: Number, default: 0 @@ -83,21 +98,78 @@ const clientDetailsSchema = new Schema({ required: true, unique: true, }, - dob: { + address: { + type: String, + }, + city: { + type: String, + }, + state: { + type: String, + }, + zip: { + type: String, + }, + billingName: { + type: String, + }, + billingEmail: { + type: String, + }, + billingAddress: { + type: String, + }, + billingCity: { + type: String, + }, + billingState: { + type: String, + }, + billingZip: { + type: String, + }, + position: { + type: String, + }, + signatoryName: { + type: String, + }, + signatoryEmail: { + type: String, + }, + paymentMethod: { type: String, }, bankName: { type: String, }, - accountHolderName: { - type: String, - }, accountNumber: { type: String, }, routingNumber: { type: String, }, + accountType: { + type: String, + }, + accountHolderName: { + type: String, + }, + cardNumber: { + type: String, + }, + expiryDate: { + type: String, + }, + securityCode: { + type: String, + }, + cardZip: { + type: String, + }, + password: { + type: String, + }, }); /* ---------------------- Client Schema ---------------------- */ @@ -239,4 +311,4 @@ const ProjectModel = model("Project", projectSchema); const LoggedModel = model("TrackLog", loggedSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel }; +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel }; \ No newline at end of file diff --git a/src/repository/vaClient.repository.js b/src/repository/vaClient.repository.js new file mode 100644 index 0000000..d087d4c --- /dev/null +++ b/src/repository/vaClient.repository.js @@ -0,0 +1,55 @@ +import { ClientModel } from "../models/vaModel.js"; + +class VaClientRepository { + async create(data) { + try { + const newClient = new ClientModel({ ...data }); + const response = await newClient.save(); + return response; + } catch (error) { + throw error; + } + } + + async findByEmail(email) { + try { + const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() }); + return response; + } catch (error) { + throw error; + } + } + + async findById(id) { + try { + const response = await ClientModel.findById(id); + return response; + } catch (error) { + throw error; + } + } + + async update(id, data) { + try { + const response = await ClientModel.findByIdAndUpdate( + id, + { $set: { ...data } }, + { new: true } + ); + return response; + } catch (error) { + throw error; + } + } + + async delete(id) { + try { + const response = await ClientModel.findByIdAndDelete(id); + return response; + } catch (error) { + throw error; + } + } +} + +export default VaClientRepository; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js new file mode 100644 index 0000000..ef7b746 --- /dev/null +++ b/src/routes/v1/client.routes.js @@ -0,0 +1,29 @@ +import express from "express"; +const clientRoutes = express.Router(); + +import { va_register_client } from "../../controllers/client/vaClient.controller.js"; +import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js"; +import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; +import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js"; +import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.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.get("/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC) +clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) + + +// // API endpoint (for frontend to call) +// clientRoutes.post("/forgot-password", va_client_request_password_reset); + +// // EJS rendering routes +// clientRoutes.get("/reset-form", va_client_render_reset_form); +// clientRoutes.post("/reset", va_client_process_reset); + + + +export default clientRoutes; \ No newline at end of file diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index e9747f7..0b30a88 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -1,6 +1,7 @@ import path from 'path'; import authRoutes from './auth.routes.js'; import vaRoutes from './va.routes.js'; +import clientRoutes from './client.routes.js'; import express from "express"; @@ -19,6 +20,7 @@ v1Routes.get("/auth", function(req, res) { v1Routes.use("/auth_callback", authRoutes); v1Routes.use("/va", vaRoutes); +v1Routes.use("/client", clientRoutes); v1Routes.get("/", function(req, res) { res.json({"connected": true}) diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index abe7a84..34cc5b2 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -3,7 +3,6 @@ const vaRoutes = express.Router(); import { va_username_callback } from "../../controllers/va/vaUsername.controller.js"; import { va_register } from "../../controllers/va/vaProfile/vaRegister.controller.js"; -import { va_register_client } from "../../controllers/va/vaClient/vaClient.controller.js"; import { va_profile_callback } from "../../controllers/va/vaProfile/vaProfile.controller.js"; import { va_profile_update_callback } from "../../controllers/va/vaProfile/vaProfileUpdate.controller.js"; import { va_login_callback } from "../../controllers/va/vaAuth/vaLogin.controller.js"; @@ -12,10 +11,7 @@ import { va_logout_callback } from "../../controllers/va/vaAuth/vaLogout.control import { va_filter_search } from "../../controllers/va/vaFilterSearch.controller.js"; import { va_log_callback } from "../../controllers/va/vaLog/vaLog.controller.js"; import { va_callback } from "../../controllers/va/vaLog/vaListLog.controller.js"; -import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js"; -import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; import { va_project_log_callback } from "../../controllers/va/vaProjects/vaProjectLog.controller.js"; -import { va_projects_callback } from "../../controllers/va/vaProjects/vaProjects.controller.js"; import { authValidator } from "../../middlewares/auth.middleware.js"; import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrackWork.controller.js"; import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js"; @@ -28,11 +24,12 @@ vaRoutes.post("/login", va_login_callback); // VA Callback for login VA vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token vaRoutes.post("/register", va_register); // Register a new VA -vaRoutes.post("/client/register", va_register_client); // Register a new VA vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile + + vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC) @@ -42,19 +39,10 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC) vaRoutes.get("/search", va_filter_search); -vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC) -vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) - - - - - // GET Windows Tab Logged Data for VA IN Between Login and LOGOUT vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function); - - // Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function); @@ -67,7 +55,12 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba // VA filter search: supports query params `name`, `email`, `from`, `to` +// // API endpoint (for frontend to call) +// vaRoutes.post("/forgot-password", va_request_password_reset); +// // EJS rendering routes +// vaRoutes.get("/reset-form", va_render_reset_form); +// vaRoutes.post("/reset", va_process_reset); export default vaRoutes; \ No newline at end of file diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js new file mode 100644 index 0000000..e54f0ec --- /dev/null +++ b/src/services/vaClient.service.js @@ -0,0 +1,107 @@ +import logger from "../config/logger.js"; + +class VaClientService { + constructor(vaClientRepository) { + this.clientRepository = vaClientRepository; + } + + async registerClient(data) { + try { + // Check if user already exists + const existingUser = await this.clientRepository.findByEmail(data.userDetails.email); + if (existingUser) { + throw new Error("User already exists with this email"); + } + + // Sanitize and prepare data + const clientData = this.sanitizeClientData(data); + + const response = await this.clientRepository.create(clientData); + return response; + } catch (error) { + logger.error("Client Service - registerClient error: " + error.message); + throw error; + } + } + + async getClientByEmail(email) { + try { + const response = await this.clientRepository.findByEmail(email); + return response; + } catch (error) { + logger.error("Client Service - getClientByEmail error: " + error.message); + throw error; + } + } + + async getClientById(id) { + try { + const response = await this.clientRepository.findById(id); + if (!response) throw new Error("Client not found"); + return response; + } catch (error) { + logger.error("Client Service - getClientById error: " + error.message); + throw error; + } + } + + async updateClient(id, data) { + try { + const response = await this.clientRepository.update(id, data); + if (!response) throw new Error("Client not found"); + return response; + } catch (error) { + logger.error("Client Service - updateClient error: " + error.message); + throw error; + } + } + + async deleteClient(id) { + try { + const response = await this.clientRepository.delete(id); + return response; + } catch (error) { + logger.error("Client Service - deleteClient error: " + error.message); + throw error; + } + } + + // Helper method to sanitize data + sanitizeClientData(payload) { + const { userDetails } = payload; + + return { + userDetails: { + name: userDetails.name, + email: userDetails.email.toLowerCase(), + address: userDetails.address || null, + city: userDetails.city || null, + state: userDetails.state || null, + zip: userDetails.zip || null, + billingName: userDetails.billingName || null, + billingAddress: userDetails.billingAddress || null, + billingCity: userDetails.billingCity || null, + billingState: userDetails.billingState || null, + billingZip: userDetails.billingZip || null, + billingEmail: userDetails.billingEmail || null, + position: userDetails.position || null, + signatoryName: userDetails.signatoryName || null, + signatoryEmail: userDetails.signatoryEmail || null, + paymentMethod: userDetails.paymentMethod || null, + bankName: userDetails.bankName || null, + accountNumber: userDetails.accountNumber || null, + routingNumber: userDetails.routingNumber || null, + accountType: userDetails.accountType || null, + accountHolderName: userDetails.accountHolderName || null, + cardNumber: userDetails.cardNumber || null, + expiryDate: userDetails.expiryDate || null, + securityCode: userDetails.securityCode || null, + cardZip: userDetails.cardZip || null, + }, + vaIds: payload.vaIds || [], + projectIds: payload.projectIds || [], + }; + } +} + +export default VaClientService; \ No newline at end of file diff --git a/views/va_client_register.ejs b/views/va_client_register.ejs index d8742a4..b9c7d30 100644 --- a/views/va_client_register.ejs +++ b/views/va_client_register.ejs @@ -3,92 +3,19 @@
-