Added va and client signup form

This commit is contained in:
Aakash Mittal 2025-11-11 19:44:32 +05:30
parent 87da60613a
commit f6e9eeffa9
18 changed files with 2297 additions and 689 deletions

View File

@ -1,222 +1,239 @@
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
} }
body { body {
background-color: #fff; background-color: #fff;
color: #333; color: #333;
} }
.container { .container {
display: flex; display: flex;
height: 100vh; height: 100vh;
width: 100%; width: 100%;
} }
/* Sidebar Styles */ /* Sidebar Styles */
.sidebar { .sidebar {
display: none; display: none;
flex-direction: column; flex-direction: column;
width: 344px; width: 344px;
background: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7); background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0);
padding: 24px; padding: 24px;
border-radius: 14px; border-radius: 14px;
margin: 20px; margin: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
.logo { .logo {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-bottom: 32px; margin-bottom: 32px;
} }
.logo-placeholder img{ .logo-placeholder img{
width: 100%; width: 100%;
height: auto; height: auto;
max-width: 196px; max-width: 196px;
} }
.logo-placeholder h2 {
font-size: 32px;
font-weight: 700;
color: #000;
text-align: center;
}
.progress-steps { .form-title-sec {
display: flex; font-size: 24px;
flex-direction: column; font-weight: 700;
gap: 8px; color: #000;
} text-align: center;
margin-bottom: 24px;
}
.step { .progress-steps {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 12px; gap: 8px;
padding: 10px 12px; }
border-radius: 12px;
border: 2px solid transparent;
cursor: pointer;
position: relative;
}
.step.active { .step {
border-color: #070065; 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 { .step.active {
background-color: #070065; border-color: #000000;
border-color: #070065; }
color: white;
}
.step-number { .step.completed .step-number {
width: 34px; background-color: #000000;
height: 34px; border-color: #000000;
border-radius: 50%; color: white;
display: flex; }
align-items: center;
justify-content: center;
border: 2px solid #a1a1aa;
font-weight: 600;
color: #a1a1aa;
transition: all 0.3s ease;
}
.step.active .step-number { .step-number {
border-color: #000; width: 34px;
color: #000; 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 { .step.active .step-number {
font-size: 14px; border-color: #000;
color: #a1a1aa; color: #000;
} }
.step.active .step-text { .step-text {
color: #000; font-size: 14px;
} color: #000000b5;
}
.step-connector { .step.active .step-text {
height: 24px; color: #000;
width: 2px; }
background-color: #a1a1aa;
margin-left: 16px;
position: absolute;
top: 46px;
left: 12px;
}
.step.active .step-connector { .step-connector {
background-color: #000; height: 24px;
} width: 2px;
background-color: #00000070;
margin-left: 16px;
position: absolute;
top: 46px;
left: 12px;
}
/* Main Content Styles */ .step.active .step-connector {
.main-content { background-color: #000;
flex: 1; }
display: flex;
flex-direction: column;
padding: 20px;
overflow-y: auto;
}
.mobile-progress { /* Main Content Styles */
background-color: #f4f4f5; .main-content {
padding: 16px; flex: 1;
border-radius: 12px; display: flex;
margin-bottom: 20px; flex-direction: column;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); padding: 20px;
overflow-x: auto; overflow-y: auto;
} }
.mobile-progress ol { .mobile-progress {
display: flex; background-color: #f4f4f5;
list-style: none; padding: 16px;
gap: 12px; border-radius: 12px;
min-width: max-content; margin-bottom: 20px;
} box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow-x: auto;
}
.mobile-progress .step { .mobile-progress ol {
flex-shrink: 0; display: flex;
} list-style: none;
gap: 12px;
min-width: max-content;
}
.form-container { .mobile-progress .step {
flex: 1; flex-shrink: 0;
display: flex; }
flex-direction: column;
align-items: center;
padding: 40px 0;
}
.form-card { .form-container {
width: 100%; flex: 1;
max-width: 450px; display: flex;
background: white; flex-direction: column;
border: 1px solid #e5e7eb; align-items: center;
border-radius: 12px; padding: 40px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); }
padding: 24px;
}
.form-title { .form-card {
text-align: center; width: 100%;
font-size: 20px; max-width: 450px;
font-weight: 600; background: white;
margin-bottom: 24px; border: 1px solid #e5e7eb;
color: #000; border-radius: 12px;
} box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
padding: 24px;
}
.form-step { .form-title {
display: none; text-align: center;
} font-size: 20px;
font-weight: 600;
margin-bottom: 24px;
color: #000;
}
.form-step.active { .form-step {
display: block; display: none;
} }
.form-group { .form-step.active {
margin-bottom: 16px; display: block;
} }
.input-container { .form-group {
position: relative; margin-bottom: 16px;
background-color: #f9fafb; }
border-radius: 6px;
overflow: hidden;
}
.input-label { .input-container {
position: absolute; position: relative;
left: 12px; background-color: #f9fafb;
top: 50%; border-radius: 6px;
transform: translateY(-50%); overflow: hidden;
font-size: 14px; padding: 2px 0;
color: #6b7280; }
transition: all 0.2s ease;
pointer-events: none;
}
.input-field, .select-field { .input-label {
width: 100%; position: absolute;
padding: 16px 12px; left: 12px;
border: none; top: 50%;
background: transparent; transform: translateY(-50%);
outline: none; font-size: 14px;
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; 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-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-repeat: no-repeat;
background-position: right 12px center; background-position: right 12px center;
background-size: 16px; background-size: 16px;
border-radius: 6px;
} }
.input-field:focus + .input-label, .input-field:focus + .input-label,
.input-field:not(:placeholder-shown) + .input-label, .input-field:not(:placeholder-shown) + .input-label,
.select-field:focus + .input-label, .select-field:focus + .input-label,
.select-field:valid + .input-label { .select-field:valid + .input-label {
top: 2px; top: 2px;
font-size: 12px; font-size: 12px;
transform: none; transform: none;
} }
.checkbox-group { .checkbox-group {
display: flex; display: flex;
@ -251,7 +268,7 @@
} }
.btn-primary { .btn-primary {
background-color: #070065; background-color: #000000;
color: white; color: white;
} }
@ -268,67 +285,84 @@
background-color: #e5e7eb; background-color: #e5e7eb;
} }
.btn:disabled { .btn-outline {
opacity: 0.6; background-color: transparent;
cursor: not-allowed; border: 1px solid #070065;
} color: #070065;
}
.payment-option { .btn-outline:hover {
border: 1px solid #e5e7eb; background-color: #070065;
border-radius: 8px; color: white;
padding: 16px; }
margin-bottom: 12px;
cursor: pointer;
}
.payment-option.selected {
border-color: #070065;
background-color: #f0f9ff;
}
.payment-option h4 { .btn:disabled {
margin-bottom: 8px; opacity: 0.6;
font-size: 16px; cursor: not-allowed;
} }
.payment-option p { .btn-sm {
font-size: 14px; padding: 6px 12px;
color: #6b7280; 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; display: none;
margin-top: 16px; margin: 4px 0 16px;
padding: 16px; padding: 16px;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 8px; border-radius: 8px;
background-color: #f9fafb; background-color: #bdbdbd;
} }
.payment-details.active { .payment-details.active {
display: block; display: block;
} }
.success-message { .success-message {
text-align: center; text-align: center;
padding: 40px 20px; padding: 40px 20px;
} }
.success-icon { .success-icon {
font-size: 48px; font-size: 48px;
color: #10b981; color: #10b981;
margin-bottom: 16px; margin-bottom: 16px;
} }
.success-message h3 { .success-message h3 {
font-size: 24px; font-size: 24px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.success-message p { .success-message p {
color: #6b7280; color: #6b7280;
margin-bottom: 24px; margin-bottom: 24px;
} }
.location-loading { .location-loading {
display: none; display: none;
@ -346,6 +380,85 @@
flex: 1; 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 */ /* Responsive styles */
@media (min-width: 1024px) { @media (min-width: 1024px) {
.sidebar { .sidebar {
@ -354,4 +467,9 @@
.mobile-progress { .mobile-progress {
display: none; display: none;
} }
.form-title-sec {
font-size: 40px;
margin-bottom: 40px;
}
} }

View File

@ -1,42 +1,121 @@
// // Example: Basic client-side validation or UX improvements // Email validation function
// document.addEventListener("DOMContentLoaded", function () { function validateEmail(email) {
// const form = document.querySelector("form"); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// form.addEventListener("submit", async (e) => { // Card type detection
// e.preventDefault(); 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 // Expiry date validation
// const form = document.querySelector("form"); function validateExpiryDate(expiryDate) {
// const formData = new FormData(form); const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/;
// const payload = { userDetails: {} }; 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) => { // CVV validation
// // handle nested names like userDetails[firstName] function validateCVV(cvv, cardType) {
// if (key.startsWith("userDetails[")) { if (!/^\d+$/.test(cvv)) {
// const innerKey = key.match(/userDetails\[(.*?)\]/)[1]; return false;
// payload.userDetails[innerKey] = value; }
// } else {
// payload[key] = value; const cvvLengths = {
// } visa: 3,
// }); mastercard: 3,
// // You could then send it via fetch: amex: 4,
// fetch("/api/v1/va/client/register", { discover: 3,
// method: "POST", diners: 3,
// body: JSON.stringify(payload), jcb: 3
// }) };
// .then((res) => res.json())
// .then((data) => { const expectedLength = cvvLengths[cardType] || 3;
// console.log("Server Response:", data); return cvv.length === expectedLength;
// alert("Form submitted successfully!"); }
// })
// .catch((err) => console.error("Error:", err)); // 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 = { const formData = {
userDetails: {} userDetails: {}
@ -45,27 +124,27 @@ const formData = {
// Current step tracking // Current step tracking
let currentStep = 1; let currentStep = 1;
// const statesByCountry = { const statesByCountry = {
// 'US': [ 'US': [
// 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware',
// 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky',
// 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi',
// 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico',
// 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania',
// 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont',
// 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming' 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'
// ], ],
// 'CA': [ 'CA': [
// 'Alberta', 'British Columbia', 'Manitoba', 'New Brunswick', 'Newfoundland and Labrador', 'Alberta', 'British Columbia', 'Manitoba', 'New Brunswick', 'Newfoundland and Labrador',
// 'Nova Scotia', 'Ontario', 'Prince Edward Island', 'Quebec', 'Saskatchewan' 'Nova Scotia', 'Ontario', 'Prince Edward Island', 'Quebec', 'Saskatchewan'
// ], ],
// 'UK': [ 'UK': [
// 'England', 'Scotland', 'Wales', 'Northern Ireland' 'England', 'Scotland', 'Wales', 'Northern Ireland'
// ], ],
// 'AU': [ 'AU': [
// 'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia' 'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia'
// ] ]
// }; };
// Function to navigate to any step (for tab navigation) // Function to navigate to any step (for tab navigation)
function goToStep(step) { function goToStep(step) {
@ -115,8 +194,8 @@ function nextStep(step) {
el.classList.add('completed'); el.classList.add('completed');
const stepNumber = el.querySelector('.step-number'); const stepNumber = el.querySelector('.step-number');
stepNumber.textContent = '✓'; stepNumber.textContent = '✓';
stepNumber.style.backgroundColor = '#070065'; stepNumber.style.backgroundColor = '#000000';
stepNumber.style.borderColor = '#070065'; stepNumber.style.borderColor = '#000000';
stepNumber.style.color = 'white'; stepNumber.style.color = 'white';
}); });
@ -158,15 +237,173 @@ function validateStep(step) {
const form = document.getElementById(getFormId(step)); const form = document.getElementById(getFormId(step));
if (form) { if (form) {
const requiredFields = form.querySelectorAll('[required]'); if (step === 5) {
requiredFields.forEach(field => { const selectedPayment = document.querySelector('.payment-option.selected');
if (!field.value.trim()) { if (!selectedPayment) {
isValid = false; isValid = false;
field.style.border = '1px solid #ef4444'; alert('Please select a payment method');
} else { return isValid;
field.style.border = '';
} }
});
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; return isValid;
@ -213,9 +450,6 @@ function saveStepData(step) {
case 5: case 5:
// Payment method already handled in selectPaymentOption // Payment method already handled in selectPaymentOption
break; break;
case 6:
formData.userDetails.paymentMethod = formData.payment?.type || '';
break;
} }
} }
@ -228,6 +462,10 @@ function selectPaymentOption(type) {
document.querySelectorAll('.payment-details').forEach(details => { document.querySelectorAll('.payment-details').forEach(details => {
details.classList.remove('active'); 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 // Add selected class to clicked option
@ -236,18 +474,23 @@ function selectPaymentOption(type) {
if (type === 'card') { if (type === 'card') {
document.getElementById('card-details').classList.add('active'); document.getElementById('card-details').classList.add('active');
formData.userDetails.paymentMethod = 'card'; formData.userDetails.paymentMethod = 'card';
formData.userDetails.cardNumber = '';
formData.userDetails.expiryDate = ''; // Add required attributes only to card fields
formData.userDetails.securityCode = ''; document.getElementById('card-number').setAttribute('required', 'true');
formData.userDetails.cardZip = ''; 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') { } else if (type === 'bank') {
document.getElementById('bank-details').classList.add('active'); document.getElementById('bank-details').classList.add('active');
formData.userDetails.paymentMethod = 'bank'; formData.userDetails.paymentMethod = 'bank';
formData.userDetails.bankName = '';
formData.userDetails.accountNumber = ''; // Add required attributes only to bank fields
formData.userDetails.routingNumber = ''; document.getElementById('bank-name').setAttribute('required', 'true');
formData.userDetails.accountType = ''; document.getElementById('account-number').setAttribute('required', 'true');
formData.userDetails.accountHolderName = ''; 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; submitBtn.disabled = true;
try { try {
// console.log('Submitting form data:', formData);
// Send data to server using fetch // 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -298,14 +542,18 @@ async function submitForm() {
el.classList.add('completed'); el.classList.add('completed');
const stepNumber = el.querySelector('.step-number'); const stepNumber = el.querySelector('.step-number');
stepNumber.textContent = '✓'; stepNumber.textContent = '✓';
stepNumber.style.backgroundColor = '#070065'; stepNumber.style.backgroundColor = '#000000';
stepNumber.style.borderColor = '#070065'; stepNumber.style.borderColor = '#000000';
stepNumber.style.color = 'white'; stepNumber.style.color = 'white';
}); });
nextStep(6); nextStep(6);
} else { } else {
// Handle server errors // 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) { } catch (error) {
// Handle network errors // 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 // Function to fetch states from API
async function fetchStates(countryCode) { async function fetchStates(countryCode) {
try { try {
@ -382,7 +585,7 @@ async function fetchStates(countryCode) {
} }
// Function to populate states dropdown // Function to populate states dropdown
function populateStates(countryCode, selectElementId) { async function populateStates(countryCode, selectElementId) {
const selectElement = document.getElementById(selectElementId); const selectElement = document.getElementById(selectElementId);
const states = statesByCountry[countryCode] || []; const states = statesByCountry[countryCode] || [];
@ -398,55 +601,131 @@ function populateStates(countryCode, selectElementId) {
option.textContent = state; option.textContent = state;
selectElement.appendChild(option); 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 to detect user location
function detectLocation() { async function detectLocation() {
// Show loading indicators // Show loading indicators
document.getElementById('state-loading').style.display = 'block'; document.getElementById('state-loading').style.display = 'block';
// Using a free IP geolocation API try {
fetch('https://ipapi.co/json/') const response = await fetch('https://ipapi.co/json/');
.then(response => response.json()) const data = await response.json();
.then(data => {
// Hide loading indicators // Hide loading indicators
document.getElementById('state-loading').style.display = 'none'; document.getElementById('state-loading').style.display = 'none';
// Populate city and state based on detected location // Populate city and state based on detected location
document.getElementById('city').value = data.city || ''; document.getElementById('city').value = data.city || '';
document.getElementById('zip').value = data.postal || ''; document.getElementById('zip').value = data.postal || '';
// Populate states dropdown for the detected country // Populate states dropdown for the detected country
populateStates(data.country_code, 'state'); await populateStates(data.country_code, 'state');
// Set the detected state // Set the detected state
if (data.region) { if (data.region) {
document.getElementById('state').value = data.region; document.getElementById('state').value = data.region;
} document.getElementById('state').dispatchEvent(new Event('change'));
}
// Also populate billing states dropdown
populateStates(data.country_code, 'billing-state'); // Also populate billing states dropdown
}) await populateStates(data.country_code, 'billing-state');
.catch(error => { } catch (error) {
console.error('Error detecting location:', error); console.error('Error detecting location:', error);
// Hide loading indicators // Hide loading indicators
document.getElementById('state-loading').style.display = 'none'; document.getElementById('state-loading').style.display = 'none';
// Fallback to US states if detection fails // Fallback to US states if detection fails
populateStates('US', 'state'); await populateStates('US', 'state');
populateStates('US', 'billing-state'); await populateStates('US', 'billing-state');
}); }
} }
// Initialize form interactions // Initialize form interactions
document.addEventListener('DOMContentLoaded', function() { 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 // Add input event listeners to handle label animation
document.querySelectorAll('.input-field').forEach(input => { document.querySelectorAll('.input-field').forEach(input => {
input.addEventListener('input', function() { input.addEventListener('input', function() {
if (this.value) { if (this.value) {
this.nextElementSibling.style.top = '2px'; this.nextElementSibling.style.top = '4px';
this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.fontSize = '12px';
this.nextElementSibling.style.transform = 'none'; this.nextElementSibling.style.transform = 'none';
} else { } else {
@ -457,7 +736,7 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
input.addEventListener('focus', function() { input.addEventListener('focus', function() {
this.nextElementSibling.style.top = '2px'; this.nextElementSibling.style.top = '4px';
this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.fontSize = '12px';
this.nextElementSibling.style.transform = 'none'; this.nextElementSibling.style.transform = 'none';
}); });
@ -475,7 +754,7 @@ document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.select-field').forEach(select => { document.querySelectorAll('.select-field').forEach(select => {
select.addEventListener('change', function() { select.addEventListener('change', function() {
if (this.value) { if (this.value) {
this.nextElementSibling.style.top = '2px'; this.nextElementSibling.style.top = '4px';
this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.fontSize = '12px';
this.nextElementSibling.style.transform = 'none'; this.nextElementSibling.style.transform = 'none';
} else { } else {
@ -486,7 +765,7 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
select.addEventListener('focus', function() { select.addEventListener('focus', function() {
this.nextElementSibling.style.top = '2px'; this.nextElementSibling.style.top = '4px';
this.nextElementSibling.style.fontSize = '12px'; this.nextElementSibling.style.fontSize = '12px';
this.nextElementSibling.style.transform = 'none'; this.nextElementSibling.style.transform = 'none';
}); });
@ -522,7 +801,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Handle use account email checkbox // Handle use account email checkbox
document.getElementById('use-account-email').addEventListener('change', function() { document.getElementById('use-account-email').addEventListener('change', function() {
if (this.checked) { 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')); document.getElementById('billing-email').dispatchEvent(new Event('input'));
} }
}); });

View File

@ -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) => { // Form data object
e.preventDefault(); const formData = {
const password = document.getElementById("password").value; userDetails: {}
const cPassword = document.getElementById("cPassword").value; };
if (password !== cPassword) { // Current step tracking
alert("Passwords do not match!"); let currentStep = 1;
return;
}
handleSubmit();
});
});
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 states = {
const form = document.querySelector("form"); 'US': [
const formData = new FormData(form); { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
const payload = { userDetails: {} }; { 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) => { // Initialize the form
// handle nested names like userDetails[firstName] document.addEventListener('DOMContentLoaded', function() {
if (key.startsWith("userDetails[")) { // Populate country dropdown
const innerKey = key.match(/userDetails\[(.*?)\]/)[1]; const countrySelect = document.getElementById('country');
payload.userDetails[innerKey] = value; countrySelect.innerHTML = '<option value=""></option>';
} else {
payload[key] = value; 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 = '<option value=""></option>';
// Simulate API delay
setTimeout(() => {
if (countryCode && states[countryCode]) {
states[countryCode].forEach(state => {
const option = document.createElement('option');
option.value = state.code;
option.textContent = state.name;
stateSelect.appendChild(option);
});
}
// Hide loading
document.getElementById('state-loading').style.display = 'none';
}, 500);
});
// 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", { // Function to navigate to any step (for tab navigation)
method: "POST", function goToStep(step) {
body: JSON.stringify(payload), // Only allow navigation to completed steps or the next step
}) if (step > currentStep && step > getMaxCompletedStep() + 1) {
.then((res) => res.json()) return;
.then((data) => { }
console.log("Server Response:", data);
alert("Form submitted successfully!"); // Hide current step
}) document.getElementById(`step-${currentStep}`).classList.remove('active');
.catch((err) => console.error("Error:", err)); 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);
// }
// });
// });
// });

View File

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

View File

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

View File

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

View File

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

View File

@ -4,17 +4,13 @@ import { VAModel } from "../../../models/vaModel.js";
// Register a new VA // Register a new VA
async function va_register(req, res) { async function va_register(req, res) {
try { try {
const payload = req.body ? JSON.parse(req.body) : {}; const payload = req.body|| {};
const { userDetails } = payload; const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) { if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) {
return res.status(400).json({ success: false, message: "Missing required fields" }); 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 // ✅ Check if user already exists by email
const existingUser = await VAModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() }); const existingUser = await VAModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() });
if (existingUser) { if (existingUser) {
@ -33,11 +29,15 @@ async function va_register(req, res) {
dob: userDetails.dob || null, dob: userDetails.dob || null,
age: userDetails.age || null, age: userDetails.age || null,
address: userDetails.address || null, address: userDetails.address || null,
city: userDetails.city || null,
state: userDetails.state || null,
country: userDetails.country || null,
password: hashedPassword, password: hashedPassword,
bankName: userDetails.bankName || null, paymentMethod: userDetails.paymentMethod || null,
accountHolderName: userDetails.accountHolderName || null, payoneerAccount: userDetails.payoneerAccount || null,
accountNumber: userDetails.accountNumber || null, veemAccount: userDetails.veemAccount || null,
routingNumber: userDetails.routingNumber || null, phone: userDetails.phone || null,
skype: userDetails.skype || null,
}, },
tracking: payload.tracking || undefined, tracking: payload.tracking || undefined,
clientIds: payload.clientIds || [], clientIds: payload.clientIds || [],

View File

@ -25,19 +25,31 @@ const userDetailsSchema = new Schema({
address: { address: {
type: String, type: String,
}, },
city: {
type: String,
},
state: {
type: String,
},
country: {
type: String,
},
password: { password: {
type: String, type: String,
}, },
bankName: { paymentMethod: {
type: String, type: String,
}, },
accountHolderName: { payoneerAccount: {
type: String, type: String,
}, },
accountNumber: { veemAccount: {
type: String, type: String,
}, },
routingNumber: { phone: {
type: String,
},
skype: {
type: String, type: String,
}, },
}); });
@ -61,6 +73,9 @@ const projectSchema = new Schema({
type: String, type: String,
required: true, required: true,
}, },
hourlyRate: {
type: String,
},
totalLoggedHours: { totalLoggedHours: {
type: Number, type: Number,
default: 0 default: 0
@ -83,21 +98,78 @@ const clientDetailsSchema = new Schema({
required: true, required: true,
unique: 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, type: String,
}, },
bankName: { bankName: {
type: String, type: String,
}, },
accountHolderName: {
type: String,
},
accountNumber: { accountNumber: {
type: String, type: String,
}, },
routingNumber: { routingNumber: {
type: String, 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 ---------------------- */ /* ---------------------- Client Schema ---------------------- */
@ -239,4 +311,4 @@ const ProjectModel = model("Project", projectSchema);
const LoggedModel = model("TrackLog", loggedSchema); const LoggedModel = model("TrackLog", loggedSchema);
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel }; export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel };

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import path from 'path'; import path from 'path';
import authRoutes from './auth.routes.js'; import authRoutes from './auth.routes.js';
import vaRoutes from './va.routes.js'; import vaRoutes from './va.routes.js';
import clientRoutes from './client.routes.js';
import express from "express"; import express from "express";
@ -19,6 +20,7 @@ v1Routes.get("/auth", function(req, res) {
v1Routes.use("/auth_callback", authRoutes); v1Routes.use("/auth_callback", authRoutes);
v1Routes.use("/va", vaRoutes); v1Routes.use("/va", vaRoutes);
v1Routes.use("/client", clientRoutes);
v1Routes.get("/", function(req, res) { v1Routes.get("/", function(req, res) {
res.json({"connected": true}) res.json({"connected": true})

View File

@ -3,7 +3,6 @@ const vaRoutes = express.Router();
import { va_username_callback } from "../../controllers/va/vaUsername.controller.js"; import { va_username_callback } from "../../controllers/va/vaUsername.controller.js";
import { va_register } from "../../controllers/va/vaProfile/vaRegister.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_callback } from "../../controllers/va/vaProfile/vaProfile.controller.js";
import { va_profile_update_callback } from "../../controllers/va/vaProfile/vaProfileUpdate.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"; 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_filter_search } from "../../controllers/va/vaFilterSearch.controller.js";
import { va_log_callback } from "../../controllers/va/vaLog/vaLog.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_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_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 { authValidator } from "../../middlewares/auth.middleware.js";
import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrackWork.controller.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"; 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.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("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA 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.get("/profile", authValidator, va_profile_callback); // Get VA Profile
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC) 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("/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 // GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function); vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA // Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function); vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
@ -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` // 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; export default vaRoutes;

View File

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

View File

@ -3,92 +3,19 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VA Registration</title> <title>Client Registration</title>
<!-- Bootstrap -->
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> -->
<link href="/public/css/va-register.css" rel="stylesheet"> <link href="/public/css/va-register.css" rel="stylesheet">
</head> </head>
<body> <body>
<!-- <section class="h-100 py-5">
<div class="container">
<div class="row justify-content-center align-items-center">
<div class="col-lg-10 col-xl-9">
<div class="card card-registration shadow-lg">
<div class="row g-0">
<div class="col-xl-5 d-none d-xl-block">
<img
src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-registration/img4.webp"
alt="Registration"
class="img-fluid"
/>
</div>
<div class="col-xl-7 bg-white">
<div class="card-body text-black">
<h3 class="text-center mb-4">DocTracker Clients Registration</h3>
<form id="vaClientForm">
<div class="mb-3">
<label class="form-label" for="firstName">Client Name/Company *</label>
<input type="text" id="firstName" name="userDetails[name]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="email">Email *</label>
<input type="email" id="email" name="userDetails[email]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="dob">Date of Birth</label>
<input type="date" id="dob" name="userDetails[dob]" class="form-control form-control-lg">
</div>
<h5>Bank Details</h5>
<div class="mb-3">
<label class="form-label" for="bankName">Bank Name</label>
<input type="text" id="bankName" name="userDetails[bankName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountHolderName">Account Holder Name</label>
<input type="text" id="accountHolderName" name="userDetails[accountHolderName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountNumber">Account Number</label>
<input type="text" id="accountNumber" name="userDetails[accountNumber]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="routingNumber">Routing Number</label>
<input type="text" id="routingNumber" name="userDetails[routingNumber]" class="form-control form-control-lg" required>
</div>
<div class="d-flex justify-content-end gap-2 pt-3">
<button type="reset" class="btn btn-light btn-lg">Reset</button>
<button type="submit" class="btn btn-warning btn-lg">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section> -->
<div class="container"> <div class="container">
<!-- Sidebar for desktop --> <!-- Sidebar for desktop -->
<div class="sidebar"> <div class="sidebar">
<div class="logo"> <div class="logo">
<div class="logo-placeholder"> <div class="logo-placeholder">
<img <h2>DocTracker</h2>
src="https://images.docva.com/logo-colored-vertical.svg"
/>
</div> </div>
</div> </div>
<nav class="progress-steps"> <nav class="progress-steps">
@ -160,6 +87,7 @@
<!-- Form container --> <!-- Form container -->
<div class="form-container"> <div class="form-container">
<h2 class="form-title-sec">Client Registration</h2>
<div class="form-card"> <div class="form-card">
<!-- Step 1: Create Account --> <!-- Step 1: Create Account -->
<div class="form-step active" id="step-1"> <div class="form-step active" id="step-1">
@ -404,7 +332,6 @@
<div class="success-icon">✓</div> <div class="success-icon">✓</div>
<h3>Registration Complete!</h3> <h3>Registration Complete!</h3>
<p>Your account has been successfully created. You will receive a confirmation email shortly.</p> <p>Your account has been successfully created. You will receive a confirmation email shortly.</p>
<button type="button" class="btn btn-primary" onclick="resetForm()">Create Another Account</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,13 +3,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VA Registration</title> <title>Virtual Assistant Registration</title>
<!-- Bootstrap --> <link href="/public/css/va-register.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
body { /* body {
background-color: #f8f9fa; background-color: #f8f9fa;
font-family: 'Segoe UI', sans-serif; font-family: 'Segoe UI', sans-serif;
} }
@ -89,117 +88,280 @@
.card-registration img { .card-registration img {
display: none; display: none;
} }
} } */
</style> </style>
</head> </head>
<body> <body>
<section class="h-100 py-5">
<div class="container"> <div class="container">
<div class="row justify-content-center align-items-center"> <!-- Sidebar for desktop -->
<div class="col-lg-10 col-xl-9"> <div class="sidebar">
<div class="card card-registration shadow-lg"> <div class="logo">
<div class="row g-0"> <div class="logo-placeholder">
<h2>DocTracker</h2>
<!-- Left Image -->
<div class="col-xl-5 d-none d-xl-block">
<img
src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-registration/img4.webp"
alt="Registration"
class="img-fluid"
/>
</div> </div>
<!-- Right Form -->
<div class="col-xl-7 bg-white">
<div class="card-body text-black">
<h3 class="text-center mb-4">Virtual Assistant Registration</h3>
<form id="vaForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="firstName">First Name *</label>
<input type="text" id="firstName" name="userDetails[firstName]" class="form-control form-control-lg" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="lastName">Last Name *</label>
<input type="text" id="lastName" name="userDetails[lastName]" class="form-control form-control-lg" required>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="email">Email *</label>
<input type="email" id="email" name="userDetails[email]" class="form-control form-control-lg" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="age">Age</label>
<input type="text" id="age" name="userDetails[age]" class="form-control form-control-lg">
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="dob">Date of Birth</label>
<input type="date" id="dob" name="userDetails[dob]" class="form-control form-control-lg">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="password">Password *</label>
<input type="password" id="password" name="userDetails[password]" class="form-control form-control-lg" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="cPassword">Confirm Password *</label>
<input type="password" id="cPassword" name="userDetails[cPassword]" class="form-control form-control-lg" required>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="address">Address</label>
<input type="text" id="address" name="userDetails[address]" class="form-control form-control-lg">
</div>
<h5>Bank Details</h5>
<div class="mb-3">
<label class="form-label" for="bankName">Bank Name</label>
<input type="text" id="bankName" name="userDetails[bankName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountHolderName">Account Holder Name</label>
<input type="text" id="accountHolderName" name="userDetails[accountHolderName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountNumber">Account Number</label>
<input type="text" id="accountNumber" name="userDetails[accountNumber]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="routingNumber">Routing Number</label>
<input type="text" id="routingNumber" name="userDetails[routingNumber]" class="form-control form-control-lg" required>
</div>
<div class="mb-4">
<label class="form-label" for="status">Status</label>
<select id="status" name="status" class="form-select form-select-lg">
<option value="FULLTIME">Full-Time</option>
<option value="PARTTIME">Part-Time</option>
</select>
</div>
<div class="d-flex justify-content-end gap-2 pt-3">
<button type="reset" class="btn btn-light btn-lg">Reset</button>
<button type="submit" class="btn btn-warning btn-lg">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div> </div>
<nav class="progress-steps">
<div class="step active" data-step="1" onclick="goToStep(1)">
<div class="step-number">1</div>
<div class="step-text">Account Setup</div>
<div class="step-connector"></div>
</div>
<div class="step" data-step="2" onclick="goToStep(2)">
<div class="step-number">2</div>
<div class="step-text">Personal Information</div>
<div class="step-connector"></div>
</div>
<div class="step" data-step="3" onclick="goToStep(3)">
<div class="step-number">3</div>
<div class="step-text">Contact Information</div>
<div class="step-connector"></div>
</div>
<div class="step" data-step="4" onclick="goToStep(4)">
<div class="step-number">4</div>
<div class="step-text">Payment Details</div>
<div class="step-connector"></div>
</div>
<div class="step" data-step="5" onclick="goToStep(5)">
<div class="step-number">5</div>
<div class="step-text">Done</div>
</div>
</nav>
</div>
<!-- Main content -->
<div class="main-content">
<!-- Mobile progress indicator -->
<div class="mobile-progress">
<ol>
<li class="step active" data-step="1" onclick="goToStep(1)">
<div class="step-number">1</div>
<div class="step-text">Account</div>
</li>
<li class="step" data-step="2" onclick="goToStep(2)">
<div class="step-number">2</div>
<div class="step-text">Personal</div>
</li>
<li class="step" data-step="3" onclick="goToStep(3)">
<div class="step-number">3</div>
<div class="step-text">Contact</div>
</li>
<li class="step" data-step="4" onclick="goToStep(4)">
<div class="step-number">4</div>
<div class="step-text">Payment</div>
</li>
<li class="step" data-step="5" onclick="goToStep(5)">
<div class="step-number">5</div>
<div class="step-text">Done</div>
</li>
</ol>
</div>
<!-- Form container -->
<div class="form-container">
<h2 class="form-title-sec">Virtual Assistant Registration</h2>
<div class="form-card">
<!-- Step 1: Account Setup -->
<div class="form-step active" id="step-1">
<h2 class="form-title">Account Setup</h2>
<form id="account-form">
<div class="form-group">
<div class="input-container">
<input type="email" id="email" class="input-field" placeholder=" " required>
<label for="email" class="input-label">Email Address *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required>
<label for="password" class="input-label">Create Password *</label>
</div>
<div class="password-strength">
<div class="password-strength-bar" id="password-strength-bar"></div>
</div>
<div class="password-actions">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
</div>
<div class="form-text">Use at least 8 characters with a mix of letters, numbers, and symbols</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required>
<label for="confirmPassword" class="input-label">Confirm Password *</label>
</div>
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
</div>
<div class="form-actions">
<div></div>
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button>
</div>
</form>
</div>
<!-- Step 2: Personal Information -->
<div class="form-step" id="step-2">
<h2 class="form-title">Personal Information</h2>
<div class="profile-incomplete" id="profile-incomplete" style="display: none;">
Profile is not yet complete, first name is missing
</div>
<form id="personal-form">
<div class="row">
<div class="form-group">
<div class="input-container">
<input type="text" id="firstName" class="input-field" placeholder=" " required>
<label for="firstName" class="input-label">First Name *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="lastName" class="input-field" placeholder=" " required>
<label for="lastName" class="input-label">Last Name *</label>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<div class="input-container">
<input type="number" id="age" class="input-field" placeholder=" " min="18" max="100">
<label for="age" class="input-label">Age</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="date" id="dob" class="input-field" placeholder=" ">
<label for="dob" class="input-label">Date of Birth</label>
</div>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="address" class="input-field" placeholder=" " required>
<label for="address" class="input-label">Address *</label>
</div>
</div>
<div class="row">
<div class="form-group">
<div class="input-container">
<input type="text" id="city" class="input-field" placeholder=" " required>
<label for="city" class="input-label">City / Town *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<select id="country" class="select-field" required>
<option value=""></option>
</select>
<label for="country" class="input-label">Country *</label>
</div>
</div>
</div>
<div class="form-group">
<div class="input-container">
<select id="state" class="select-field" required>
<option value=""></option>
</select>
<label for="state" class="input-label">State / Region *</label>
<div class="location-loading" id="state-loading">Loading states...</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)">Continue</button>
</div>
</form>
</div>
<!-- Step 3: Contact Information -->
<div class="form-step" id="step-3">
<h2 class="form-title">Contact Information</h2>
<form id="contact-form">
<div class="form-group">
<label for="phone" class="input-label">Phone Number *</label>
<div class="input-container">
<input type="tel" id="phone" class="input-field" placeholder=" " required>
<label for="phone" class="input-label" style="left: 40px;">Phone Number</label>
<span style="position: absolute; left: 12px; top: 50%; transform: translateY(-50%);">+</span>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="skype" class="input-field" placeholder=" ">
<label for="skype" class="input-label">Skype ID</label>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(4)">Continue</button>
</div>
</form>
</div>
<!-- Step 4: Payment Details -->
<div class="form-step" id="step-4">
<h2 class="form-title">Payment Details</h2>
<form id="payment-form">
<div class="form-group">
<label class="input-label">Payment Method *</label>
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
</div>
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
</div>
</div>
</div>
<div class="payment-option" onclick="selectPaymentMethod('veem')">
<h4>Veem</h4>
<p>Receive payments via Veem</p>
</div>
<div id="veem-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="veemAccount" class="input-field" placeholder=" " required>
<label for="veemAccount" class="input-label">Veem Account Number *</label>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Back</button>
<button type="button" class="btn btn-primary" onclick="submitForm()">Complete Registration</button>
</div>
</form>
</div>
<!-- Step 5: Success -->
<div class="form-step" id="step-5">
<div class="success-message">
<div class="success-icon">✓</div>
<h3>Registration Complete!</h3>
<p>Your Virtual Assistant account has been successfully created..</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</section>
<script src="/public/js/va-register.js"></script> <script src="/public/js/va-register.js"></script>
</body> </body>
</html> </html>