diff --git a/package-lock.json b/package-lock.json index a773578..959445e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,8 +45,11 @@ "@babel/cli": "^7.26.4", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", + "autoprefixer": "^10.4.21", "babel-loader": "^9.2.1", "copy-webpack-plugin": "^12.0.2", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", "terser-webpack-plugin": "^5.3.11", "webpack": "^5.97.1", "webpack-cli": "^6.0.1", @@ -1828,9 +1831,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2546,6 +2549,44 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -3015,9 +3056,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001692", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", - "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -3973,9 +4014,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", - "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -4726,6 +4767,20 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -6604,6 +6659,25 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "license": "ISC" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -6782,6 +6856,16 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -7244,6 +7328,42 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -8423,6 +8543,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -8589,6 +8719,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/public/css/va-register.css b/public/css/va-register.css index fa53161..62fb0ce 100644 --- a/public/css/va-register.css +++ b/public/css/va-register.css @@ -1,70 +1,475 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; +} + body { - background-color: #212529; + background-color: #fff; + color: #333; } -.card-registration { - border-radius: 15px; -} - -.card-registration .form-control { - border-radius: 0.5rem; -} - -.form-label { - font-weight: 500; -} - -.btn-warning { - background-color: #ffb300; - border: none; - color: #000; - font-weight: 600; -} - -.btn-warning:hover { - background-color: #e6a000; -} - -.form-outline .form-control+.form-label { - position: absolute; - top: 6px; - max-width: 90%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - left: .75rem; - padding-top: .37rem; - pointer-events: none; - transform-origin: 0 0; - transition: all .2s ease-out; - color: var(--mdb-form-control-label-color); - margin-bottom: 0; - background-color: #ffffff; -} -.form-outline .form-control { - outline: none; - box-shadow: none; -} - -.form-outline { - position: relative; +.container { + display: flex; + height: 100vh; width: 100%; } -.form-outline .form-control.not-required:placeholder-shown + .form-label, -.form-outline .form-control.not-required:empty + .form-label { - top: 6px; + +/* Sidebar Styles */ +.sidebar { + display: none; + flex-direction: column; + width: 344px; + background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0); + padding: 24px; + border-radius: 14px; + margin: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } -.form-outline .form-control:focus + .form-label, -.form-outline .form-control:valid + .form-label, -.form-outline .form-control.not-required:not(:placeholder-shown) + .form-label, -.form-outline .form-control.not-required:focus + .form-label { - top: -17px; +.logo { + display: flex; + justify-content: center; + margin-bottom: 32px; +} +.logo-placeholder img{ + width: 100%; + height: auto; + max-width: 196px; +} +.logo-placeholder h2 { + font-size: 32px; + font-weight: 700; + color: #000; + text-align: center; } -.card-registration .select-input.form-control[readonly]:not([disabled]) { - font-size: 1rem; - line-height: 2.15; - padding-left: .75em; - padding-right: .75em; -} \ No newline at end of file +.form-title-sec { + font-size: 24px; + font-weight: 700; + color: #000; + text-align: center; + margin-bottom: 24px; +} + +.progress-steps { + display: flex; + flex-direction: column; + gap: 8px; +} + +.step { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 12px; + border: 2px solid transparent; + cursor: pointer; + position: relative; +} + +.step.active { + border-color: #000000; +} + +.step.completed .step-number { + background-color: #000000; + border-color: #000000; + color: white; +} + +.step-number { + width: 34px; + height: 34px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #00000070; + font-weight: 600; + color: #000000b5; + transition: all 0.3s ease; +} + +.step.active .step-number { + border-color: #000; + color: #000; +} + +.step-text { + font-size: 14px; + color: #000000b5; +} + +.step.active .step-text { + color: #000; +} + +.step-connector { + height: 24px; + width: 2px; + background-color: #00000070; + margin-left: 16px; + position: absolute; + top: 46px; + left: 12px; +} + +.step.active .step-connector { + background-color: #000; +} + +/* Main Content Styles */ +.main-content { + flex: 1; + display: flex; + flex-direction: column; + padding: 20px; + overflow-y: auto; +} + +.mobile-progress { + background-color: #f4f4f5; + padding: 16px; + border-radius: 12px; + margin-bottom: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + overflow-x: auto; +} + +.mobile-progress ol { + display: flex; + list-style: none; + gap: 12px; + min-width: max-content; +} + +.mobile-progress .step { + flex-shrink: 0; +} + +.form-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 0; +} + +.form-card { + width: 100%; + max-width: 450px; + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + padding: 24px; +} + +.form-title { + text-align: center; + font-size: 20px; + font-weight: 600; + margin-bottom: 24px; + color: #000; +} + +.form-step { + display: none; +} + +.form-step.active { + display: block; +} + +.form-group { + margin-bottom: 16px; +} + +.input-container { + position: relative; + background-color: #f9fafb; + border-radius: 6px; + overflow: hidden; + padding: 2px 0; +} + +.input-label { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 14px; + color: #6b7280; + transition: all 0.2s ease; + pointer-events: none; +} + +.input-field, .select-field { + width: 100%; + padding: 18px 12px 14px; + border: none; + background: transparent; + outline: none; + font-size: 14px; + border-radius: 6px; +} + +.select-field { + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 16px; + border-radius: 6px; +} + +.input-field:focus + .input-label, +.input-field:not(:placeholder-shown) + .input-label, +.select-field:focus + .input-label, +.select-field:valid + .input-label { + top: 2px; + font-size: 12px; + transform: none; +} + + .checkbox-group { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + } + + .checkbox-group input { + width: 16px; + height: 16px; + } + + .checkbox-group label { + font-size: 14px; + } + + .form-actions { + display: flex; + justify-content: space-between; + margin-top: 24px; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + } + + .btn-primary { + background-color: #000000; + color: white; + } + + .btn-primary:hover { + opacity: 0.9; + } + + .btn-secondary { + background-color: #f3f4f6; + color: #374151; + } + + .btn-secondary:hover { + background-color: #e5e7eb; + } + +.btn-outline { + background-color: transparent; + border: 1px solid #070065; + color: #070065; +} + +.btn-outline:hover { + background-color: #070065; + color: white; +} + + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-sm { + padding: 6px 12px; + font-size: 12px; +} + +.payment-option { + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; + margin-bottom: 12px; + cursor: pointer; +} + +.payment-option.selected { + border-color: #000000; + background-color: #f0f9ff; +} + +.payment-option h4 { + margin-bottom: 8px; + font-size: 16px; +} + +.payment-option p { + font-size: 14px; + color: #6b7280; +} + +.payment-details { + display: none; + margin: 4px 0 16px; + padding: 16px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background-color: #bdbdbd; +} + +.payment-details.active { + display: block; +} + +.success-message { + text-align: center; + padding: 40px 20px; +} + +.success-icon { + font-size: 48px; + color: #10b981; + margin-bottom: 16px; +} + +.success-message h3 { + font-size: 24px; + margin-bottom: 8px; +} + +.success-message p { + color: #6b7280; + margin-bottom: 24px; +} + +.location-loading { + display: none; + font-size: 12px; + color: #6b7280; + margin-top: 4px; +} + +.row { + display: flex; + gap: 12px; +} + +.row .form-group { + flex: 1; +} + +.profile-incomplete { + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1.5rem; + color: #856404; +} + +.password-strength { + height: 5px; + background: #e9ecef; + border-radius: 5px; + margin-top: 5px; + overflow: hidden; +} + +.password-strength-bar { + height: 100%; + width: 0; + border-radius: 5px; + transition: width 0.3s ease; +} + +.password-weak { + background: #dc3545; + width: 25%; +} + +.password-medium { + background: #ffc107; + width: 50%; +} + +.password-strong { + background: #28a745; + width: 100%; +} + +.password-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; +} + +.password-toggle { + background: none; + border: none; + color: #070065; + cursor: pointer; + font-size: 12px; +} + +.password-generate { + background: none; + border: none; + color: #070065; + cursor: pointer; + font-size: 12px; + text-decoration: underline; +} + +.form-text { + font-size: 12px; + color: #6b7280; + margin-top: 4px; +} + +.detect-location { + background: none; + border: none; + color: #070065; + cursor: pointer; + font-size: 12px; + text-decoration: underline; + margin-top: 5px; +} + + /* Responsive styles */ + @media (min-width: 1024px) { + .sidebar { + display: flex; + } + .mobile-progress { + display: none; + } + + .form-title-sec { + font-size: 40px; + margin-bottom: 40px; + } + } \ No newline at end of file diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js new file mode 100644 index 0000000..deaae29 --- /dev/null +++ b/public/js/va-client-register.js @@ -0,0 +1,810 @@ +// Email validation function +function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +// Card type detection +function detectCardType(cardNumber) { + // Remove spaces and dashes + const cleanNumber = cardNumber.replace(/[\s-]/g, ''); + + 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'; +} + +// 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; +} + +// Expiry date validation +function validateExpiryDate(expiryDate) { + const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/; + if (!expiryRegex.test(expiryDate)) { + return false; + } + + const [month, year] = expiryDate.split('/'); + const expiry = new Date(2000 + parseInt(year), parseInt(month)); + const currentDate = new Date(); + const currentMonth = new Date(currentDate.getFullYear(), currentDate.getMonth()); + + return expiry >= currentMonth; +} + +// CVV validation +function validateCVV(cvv, cardType) { + if (!/^\d+$/.test(cvv)) { + return false; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + return cvv.length === expectedLength; +} + +// ZIP code validation +function validateZipCode(zipCode) { + return /^\d{5}(-\d{4})?$/.test(zipCode); +} + +// Bank account validation +function validateBankAccount(accountNumber) { + return /^\d{8,17}$/.test(accountNumber.replace(/\s/g, '')); +} + +// Routing number validation (basic US routing number check) +function validateRoutingNumber(routingNumber) { + const cleanRouting = routingNumber.replace(/\s/g, ''); + + if (!/^\d{9}$/.test(cleanRouting)) { + return false; + } + + // Simple checksum validation + const digits = cleanRouting.split('').map(Number); + const sum = (3 * (digits[0] + digits[3] + digits[6])) + + (7 * (digits[1] + digits[4] + digits[7])) + + (digits[2] + digits[5] + digits[8]); + + return sum % 10 === 0; +} + +const formData = { + userDetails: {} +}; + +// Current step tracking +let currentStep = 1; + +const statesByCountry = { + 'US': [ + 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', + 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', + 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', + 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', + 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', + 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', + 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming' + ], + 'CA': [ + 'Alberta', 'British Columbia', 'Manitoba', 'New Brunswick', 'Newfoundland and Labrador', + 'Nova Scotia', 'Ontario', 'Prince Edward Island', 'Quebec', 'Saskatchewan' + ], + 'UK': [ + 'England', 'Scotland', 'Wales', 'Northern Ireland' + ], + 'AU': [ + 'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia' + ] +}; + +// Function to navigate to any step (for tab navigation) +function goToStep(step) { + // Only allow navigation to completed steps or the next step + if (step > currentStep && step > getMaxCompletedStep() + 1) { + return; + } + + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + // Show target step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.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'; + }); + + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.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'); + }); + + 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'); + }); + + // Show previous step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.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) { + if (step === 5) { + const selectedPayment = document.querySelector('.payment-option.selected'); + if (!selectedPayment) { + isValid = false; + alert('Please select a payment method'); + return isValid; + } + + const activePaymentDetails = document.querySelector('.payment-details.active'); + if (activePaymentDetails) { + const requiredFields = activePaymentDetails.querySelectorAll('[required]'); + + // Validate card payment + if (formData.userDetails.paymentMethod === 'card') { + // Card number validation + const cardNumber = document.getElementById('card-number').value; + if (!validateCardNumber(cardNumber)) { + isValid = false; + document.getElementById('card-number').style.border = '1px solid #ef4444'; + alert('Please enter a valid card number'); + } else { + document.getElementById('card-number').style.border = ''; + // Show card type + const cardType = detectCardType(cardNumber); + if (cardType !== 'unknown') { + // You can display card type to user if needed + console.log('Detected card type:', cardType); + } + } + + // Expiry date validation + const expiryDate = document.getElementById('expiry-date').value; + if (!validateExpiryDate(expiryDate)) { + isValid = false; + document.getElementById('expiry-date').style.border = '1px solid #ef4444'; + alert('Please enter a valid expiry date (MM/YY)'); + } else { + document.getElementById('expiry-date').style.border = ''; + } + + // CVV validation + const cvv = document.getElementById('security-code').value; + const cardType = detectCardType(cardNumber); + if (!validateCVV(cvv, cardType)) { + isValid = false; + document.getElementById('security-code').style.border = '1px solid #ef4444'; + alert(`Please enter a valid ${cardType === 'amex' ? '4-digit' : '3-digit'} CVV`); + } else { + document.getElementById('security-code').style.border = ''; + } + + // ZIP code validation + const zipCode = document.getElementById('card-zip').value; + if (!validateZipCode(zipCode)) { + isValid = false; + document.getElementById('card-zip').style.border = '1px solid #ef4444'; + alert('Please enter a valid ZIP code'); + } else { + document.getElementById('card-zip').style.border = ''; + } + + } + // Validate bank payment + else if (formData.userDetails.paymentMethod === 'bank') { + // Account number validation + const accountNumber = document.getElementById('account-number').value; + if (!validateBankAccount(accountNumber)) { + isValid = false; + document.getElementById('account-number').style.border = '1px solid #ef4444'; + alert('Please enter a valid account number (8-17 digits)'); + } else { + document.getElementById('account-number').style.border = ''; + } + + // Routing number validation + const routingNumber = document.getElementById('routing-number').value; + if (!validateRoutingNumber(routingNumber)) { + isValid = false; + document.getElementById('routing-number').style.border = '1px solid #ef4444'; + alert('Please enter a valid routing number'); + } else { + document.getElementById('routing-number').style.border = ''; + } + + // Basic validation for other bank fields + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } + } + } else if (step === 1) { + const email = document.getElementById('email').value; + if (!validateEmail(email)) { + isValid = false; + document.getElementById('email').style.border = '1px solid #ef4444'; + alert('Please enter a valid email address'); + } else { + document.getElementById('email').style.border = ''; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (field.id !== 'email' && !field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else if (field.id !== 'email') { + field.style.border = ''; + } + }); + } else if (step === 3) { + const email = document.getElementById('billing-email').value; + if (!validateEmail(email)) { + isValid = false; + document.getElementById('billing-email').style.border = '1px solid #ef4444'; + alert('Please enter a valid email address'); + } else { + document.getElementById('billing-email').style.border = ''; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (field.id !== 'email' && !field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else if (field.id !== 'email') { + field.style.border = ''; + } + }); + } else if (step === 4) { + const email = document.getElementById('signatory-email').value; + if (!validateEmail(email)) { + isValid = false; + document.getElementById('signatory-email').style.border = '1px solid #ef4444'; + alert('Please enter a valid email address'); + } else { + document.getElementById('signatory-email').style.border = ''; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (field.id !== 'email' && !field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else if (field.id !== 'email') { + field.style.border = ''; + } + }); + } else { + // For other steps, validate all required fields normally + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } + } + + return isValid; +} + +// Function to get form ID based on step +function getFormId(step) { + switch(step) { + case 1: return 'account-form'; + case 2: return 'business-form'; + case 3: return 'billing-form'; + case 4: return 'signatory-form'; + case 5: return 'payment-form'; + default: return ''; + } +} + +// Function to save step data to formData object +function saveStepData(step) { + switch(step) { + case 1: + formData.userDetails.name = document.getElementById('company-name').value; + formData.userDetails.email = document.getElementById('email').value; + break; + case 2: + formData.userDetails.address = document.getElementById('address').value; + formData.userDetails.city = document.getElementById('city').value; + formData.userDetails.state = document.getElementById('state').value; + formData.userDetails.zip = document.getElementById('zip').value; + break; + case 3: + formData.userDetails.billingName = document.getElementById('billing-name').value; + formData.userDetails.billingAddress = document.getElementById('billing-address').value; + formData.userDetails.billingCity = document.getElementById('billing-city').value; + formData.userDetails.billingState = document.getElementById('billing-state').value; + formData.userDetails.billingZip = document.getElementById('billing-zip').value; + formData.userDetails.billingEmail = document.getElementById('billing-email').value; + break; + case 4: + formData.userDetails.position = document.getElementById('position').value; + formData.userDetails.signatoryName = document.getElementById('signatory-name').value; + formData.userDetails.signatoryEmail = document.getElementById('signatory-email').value; + break; + case 5: + // Payment method already handled in selectPaymentOption + break; + } +} + +// Function to select payment option +function selectPaymentOption(type) { + // Remove selected class from all options + document.querySelectorAll('.payment-option').forEach(option => { + option.classList.remove('selected'); + }); + + document.querySelectorAll('.payment-details').forEach(details => { + details.classList.remove('active'); + // Remove required attributes from all payment fields + details.querySelectorAll('[required]').forEach(field => { + field.removeAttribute('required'); + }); + }); + + // Add selected class to clicked option + event.currentTarget.classList.add('selected'); + + if (type === 'card') { + document.getElementById('card-details').classList.add('active'); + formData.userDetails.paymentMethod = 'card'; + + // Add required attributes only to card fields + document.getElementById('card-number').setAttribute('required', 'true'); + document.getElementById('expiry-date').setAttribute('required', 'true'); + document.getElementById('security-code').setAttribute('required', 'true'); + document.getElementById('card-zip').setAttribute('required', 'true'); + + } else if (type === 'bank') { + document.getElementById('bank-details').classList.add('active'); + formData.userDetails.paymentMethod = 'bank'; + + // Add required attributes only to bank fields + document.getElementById('bank-name').setAttribute('required', 'true'); + document.getElementById('account-number').setAttribute('required', 'true'); + document.getElementById('routing-number').setAttribute('required', 'true'); + document.getElementById('account-type').setAttribute('required', 'true'); + document.getElementById('account-holder-name').setAttribute('required', 'true'); + } +} + +// Function to submit the form +async function submitForm() { + // Validate current step + if (!validateStep(currentStep)) { + return; + } + + // Save current step data + saveStepData(currentStep); + + // Save payment details based on selected method + if (formData.userDetails.paymentMethod === 'card') { + formData.userDetails.cardNumber = document.getElementById('card-number').value; + formData.userDetails.expiryDate = document.getElementById('expiry-date').value; + formData.userDetails.securityCode = document.getElementById('security-code').value; + formData.userDetails.cardZip = document.getElementById('card-zip').value; + } else if (formData.userDetails.paymentMethod === 'bank') { + formData.userDetails.bankName = document.getElementById('bank-name').value; + formData.userDetails.accountNumber = document.getElementById('account-number').value; + formData.userDetails.routingNumber = document.getElementById('routing-number').value; + formData.userDetails.accountType = document.getElementById('account-type').value; + formData.userDetails.accountHolderName = document.getElementById('account-holder-name').value; + } + + // Show loading state + const submitBtn = document.querySelector('#payment-form .btn-primary'); + const originalText = submitBtn.textContent; + submitBtn.textContent = 'Submitting...'; + submitBtn.disabled = true; + + try { + // console.log('Submitting form data:', formData); + // Send data to server using fetch + const response = await fetch('/api/v1/client/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'; + }); + nextStep(6); + } else { + // Handle server errors + if (response.status === 409) { + alert('A user with this email already exists. Please use a different email.'); + } else { + alert('There was an error submitting your registration. Please try again.'); + } + } + } catch (error) { + // Handle network errors + 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; + } +} + +// Function to fetch states from API +async function fetchStates(countryCode) { + try { + // Replace with your actual API endpoint + const response = await fetch(`/api/states/${countryCode}`); + if (response.ok) { + const states = await response.json(); + return states; + } else { + console.error('Failed to fetch states'); + return []; + } + } catch (error) { + console.error('Error fetching states:', error); + return []; + } +} + +// Function to populate states dropdown +async function populateStates(countryCode, selectElementId) { + const selectElement = document.getElementById(selectElementId); + const states = statesByCountry[countryCode] || []; + + // Clear existing options except the first empty one + while (selectElement.options.length > 1) { + selectElement.remove(1); + } + + // Add states as options + states.forEach(state => { + const option = document.createElement('option'); + option.value = state; + option.textContent = state; + selectElement.appendChild(option); + }); + // const selectElement = document.getElementById(selectElementId); + + // // Clear existing options except the first empty one + // while (selectElement.options.length > 1) { + // selectElement.remove(1); + // } + + // // Fetch states from API + // const states = await fetchStates(countryCode); + + // // Add states as options + // states.forEach(state => { + // const option = document.createElement('option'); + // option.value = state.code || state.name; + // option.textContent = state.name; + // selectElement.appendChild(option); + // }); +} + +// Function to detect user location +async function detectLocation() { + // Show loading indicators + document.getElementById('state-loading').style.display = 'block'; + + try { + const response = await fetch('https://ipapi.co/json/'); + const data = await response.json(); + + // Hide loading indicators + document.getElementById('state-loading').style.display = 'none'; + + // Populate city and state based on detected location + document.getElementById('city').value = data.city || ''; + document.getElementById('zip').value = data.postal || ''; + + // Populate states dropdown for the detected country + await populateStates(data.country_code, 'state'); + + // Set the detected state + if (data.region) { + document.getElementById('state').value = data.region; + document.getElementById('state').dispatchEvent(new Event('change')); + } + + // Also populate billing states dropdown + await populateStates(data.country_code, 'billing-state'); + } catch (error) { + console.error('Error detecting location:', error); + + // Hide loading indicators + document.getElementById('state-loading').style.display = 'none'; + + // Fallback to US states if detection fails + await populateStates('US', 'state'); + await populateStates('US', 'billing-state'); + } +} + + +// Initialize form interactions +document.addEventListener('DOMContentLoaded', function() { + // Real-time card number formatting and validation + const cardNumberInput = document.getElementById('card-number'); + if (cardNumberInput) { + cardNumberInput.addEventListener('input', function(e) { + // Format card number with spaces + let value = e.target.value.replace(/\s/g, ''); + let formattedValue = ''; + + for (let i = 0; i < value.length; i++) { + if (i > 0 && i % 4 === 0) { + formattedValue += ' '; + } + formattedValue += value[i]; + } + + e.target.value = formattedValue; + + // Real-time card type detection + const cardType = detectCardType(value); + if (cardType !== 'unknown') { + // You can add visual feedback for card type here + console.log('Card type:', cardType); + } + }); + } + + // Real-time expiry date formatting + const expiryInput = document.getElementById('expiry-date'); + if (expiryInput) { + expiryInput.addEventListener('input', function(e) { + let value = e.target.value.replace(/\D/g, ''); + if (value.length >= 2) { + value = value.substring(0, 2) + '/' + value.substring(2, 4); + } + e.target.value = value; + }); + } + + // Real-time CVV validation + const cvvInput = document.getElementById('security-code'); + if (cvvInput) { + cvvInput.addEventListener('input', function(e) { + e.target.value = e.target.value.replace(/\D/g, '').substring(0, 4); + }); + } + + // Real-time email validation + const emailInputs = document.querySelectorAll('input[type="email"]'); + emailInputs.forEach(emailInput => { + if (emailInput) { + emailInput.addEventListener('blur', function(e) { + if (e.target.value && !validateEmail(e.target.value)) { + e.target.style.border = '1px solid #ef4444'; + } else { + e.target.style.border = ''; + } + }); + } + }); + // Add input event listeners to handle label animation + document.querySelectorAll('.input-field').forEach(input => { + input.addEventListener('input', function() { + if (this.value) { + this.nextElementSibling.style.top = '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%)'; + } + }); + }); + + // Handle use business address checkbox + document.getElementById('use-business-address').addEventListener('change', function() { + if (this.checked) { + document.getElementById('billing-name').value = formData.userDetails.name || ''; + document.getElementById('billing-address').value = formData.userDetails.address || ''; + document.getElementById('billing-city').value = formData.userDetails.city || ''; + document.getElementById('billing-state').value = formData.userDetails.state || ''; + document.getElementById('billing-zip').value = formData.userDetails.zip || ''; + + // Trigger input events to update labels + document.querySelectorAll('#billing-form .input-field').forEach(input => { + input.dispatchEvent(new Event('input')); + }); + + // Trigger change event for select + document.getElementById('billing-state').dispatchEvent(new Event('change')); + } + }); + + // Handle use account email checkbox + document.getElementById('use-account-email').addEventListener('change', function() { + if (this.checked) { + document.getElementById('billing-email').value = formData.userDetails.email || ''; + document.getElementById('billing-email').dispatchEvent(new Event('input')); + } + }); + // Detect user location when the page loads + detectLocation(); +}); \ No newline at end of file diff --git a/public/js/va-register.js b/public/js/va-register.js index 2fdb4fb..2a1fe50 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -1,46 +1,733 @@ -// Example: Basic client-side validation or UX improvements -document.addEventListener("DOMContentLoaded", function () { - const form = document.querySelector("form"); - form.addEventListener("submit", async (e) => { - e.preventDefault(); - const password = document.getElementById("password").value; - const cPassword = document.getElementById("cPassword").value; +// Form data object + const formData = { + userDetails: {} + }; - if (password !== cPassword) { - alert("Passwords do not match!"); - return; - } - - handleSubmit(); - }); -}); + // Current step tracking + let currentStep = 1; -function handleSubmit() { + // Countries and states data + const countries = [ + { code: 'US', name: 'United States' }, + { code: 'CA', name: 'Canada' }, + { code: 'GB', name: 'United Kingdom' }, + { code: 'AU', name: 'Australia' }, + { code: 'IN', name: 'India' }, + { code: 'DE', name: 'Germany' }, + { code: 'FR', name: 'France' }, + { code: 'BR', name: 'Brazil' }, + { code: 'JP', name: 'Japan' }, + { code: 'CN', name: 'China' } + ]; - // Example: collect form data manually - const form = document.querySelector("form"); - const formData = new FormData(form); - const payload = { userDetails: {} }; + const states = { + 'US': [ + { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' }, + { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' }, + { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' }, + { code: 'MI', name: 'Michigan' } + ], + 'CA': [ + { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' }, + { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' } + ], + 'GB': [ + { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' }, + { code: 'NIR', name: 'Northern Ireland' } + ], + 'AU': [ + { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' }, + { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' } + ], + 'IN': [ + { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' }, + { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' } + ], + 'DE': [ + { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' }, + { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' } + ], + 'FR': [ + { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' }, + { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' }, + { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' } + ], + 'BR': [ + { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' }, + { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' } + ], + 'JP': [ + { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' }, + { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' } + ], + 'CN': [ + { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' }, + { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' } + ] + }; - formData.forEach((value, key) => { - // handle nested names like userDetails[firstName] - if (key.startsWith("userDetails[")) { - const innerKey = key.match(/userDetails\[(.*?)\]/)[1]; - payload.userDetails[innerKey] = value; - } else { - payload[key] = value; - } + // Initialize the form + document.addEventListener('DOMContentLoaded', function() { + // Populate country dropdown + const countrySelect = document.getElementById('country'); + countrySelect.innerHTML = ''; + + countries.forEach(country => { + const option = document.createElement('option'); + option.value = country.code; + option.textContent = country.name; + countrySelect.appendChild(option); + }); + + // Add event listener for country change + countrySelect.addEventListener('change', function() { + const countryCode = this.value; + const stateSelect = document.getElementById('state'); + + // Show loading + document.getElementById('state-loading').style.display = 'block'; + + // Clear current states + stateSelect.innerHTML = ''; + + // Simulate API delay + setTimeout(() => { + if (countryCode && states[countryCode]) { + states[countryCode].forEach(state => { + const option = document.createElement('option'); + option.value = state.code; + option.textContent = state.name; + stateSelect.appendChild(option); + }); + } + + // Hide loading + document.getElementById('state-loading').style.display = 'none'; + }, 500); + }); + + // Password strength indicator + document.getElementById('password').addEventListener('input', function() { + updatePasswordStrength(this.value); + }); + + // Add input event listeners to handle label animation + document.querySelectorAll('.input-field').forEach(input => { + input.addEventListener('input', function() { + if (this.value) { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + } else { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + + input.addEventListener('focus', function() { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + }); + + input.addEventListener('blur', function() { + if (!this.value) { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + }); + + // Handle select field label animation + document.querySelectorAll('.select-field').forEach(select => { + select.addEventListener('change', function() { + if (this.value) { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + } else { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + + select.addEventListener('focus', function() { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + }); + + select.addEventListener('blur', function() { + if (!this.value) { + this.nextElementSibling.style.top = '50%'; + this.nextElementSibling.style.fontSize = '14px'; + this.nextElementSibling.style.transform = 'translateY(-50%)'; + } + }); + }); + + // Step navigation for desktop + document.querySelectorAll('.step').forEach(step => { + step.addEventListener('click', function() { + const stepNum = parseInt(this.getAttribute('data-step')); + if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + goToStep(stepNum); + } + }); + }); + + // Step navigation for mobile + document.querySelectorAll('.mobile-step').forEach(step => { + step.addEventListener('click', function() { + const stepNum = parseInt(this.getAttribute('data-step')); + if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + goToStep(stepNum); + } + }); + }); }); - // You could then send it via fetch: - fetch("/api/v1/va/register", { - method: "POST", - body: JSON.stringify(payload), - }) - .then((res) => res.json()) - .then((data) => { - console.log("Server Response:", data); - alert("Form submitted successfully!"); - }) - .catch((err) => console.error("Error:", err)); -} + + // Function to navigate to any step (for tab navigation) + function goToStep(step) { + // Only allow navigation to completed steps or the next step + if (step > currentStep && step > getMaxCompletedStep() + 1) { + return; + } + + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + // Show target step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + currentStep = step; + } + + // Function to get the maximum completed step + function getMaxCompletedStep() { + const completedSteps = document.querySelectorAll('.step.completed'); + let maxStep = 0; + completedSteps.forEach(step => { + const stepNum = parseInt(step.getAttribute('data-step')); + if (stepNum > maxStep) maxStep = stepNum; + }); + return maxStep; + } + + // Function to navigate to next step + function nextStep(step) { + // Validate current step before proceeding + if (!validateStep(currentStep)) { + return; + } + + // Save current step data + saveStepData(currentStep); + + // Mark current step as completed + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + // Show next step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + currentStep = step; + } + + // Function to navigate to previous step + function prevStep(step) { + // Hide current step + document.getElementById(`step-${currentStep}`).classList.remove('active'); + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.remove('active'); + }); + + // Show previous step + document.getElementById(`step-${step}`).classList.add('active'); + document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => { + el.classList.add('active'); + }); + + currentStep = step; + } + + // Function to validate current step + function validateStep(step) { + let isValid = true; + const form = document.getElementById(getFormId(step)); + + if (form) { + // For account setup step + if (step === 1) { + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + // Email validation + if (!validateEmail(email)) { + alert('Please enter a valid email address'); + isValid = false; + } + + // Password validation + if (password.length < 8) { + alert('Password must be at least 8 characters long'); + isValid = false; + } + + if (password !== confirmPassword) { + document.getElementById('password-match-error').style.display = 'block'; + isValid = false; + } else { + document.getElementById('password-match-error').style.display = 'none'; + } + } + + // For personal information step + if (step === 2) { + const firstName = document.getElementById('firstName').value; + if (!firstName) { + document.getElementById('profile-incomplete').style.display = 'block'; + isValid = false; + } else { + document.getElementById('profile-incomplete').style.display = 'none'; + } + + // Age validation + const age = document.getElementById('age').value; + if (age && (age < 18 || age > 100)) { + alert('You must be 18 years or older to register'); + isValid = false; + } + + // Country and state validation + const country = document.getElementById('country').value; + const state = document.getElementById('state').value; + + if (!country) { + alert('Please select your country'); + isValid = false; + } + + if (!state) { + alert('Please select your state/region'); + isValid = false; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } + + // For payment step + if (step === 4) { + const selectedPayment = document.querySelector('.payment-option.selected'); + if (!selectedPayment) { + alert('Please select a payment method'); + isValid = false; + } else { + const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; + if (paymentMethod === 'payoneer') { + const payoneerAccount = document.getElementById('payoneerAccount').value; + if (!payoneerAccount) { + alert('Please enter your Payoneer account number'); + isValid = false; + } + } else if (paymentMethod === 'veem') { + const veemAccount = document.getElementById('veemAccount').value; + if (!veemAccount) { + alert('Please enter your Veem account number'); + isValid = false; + } + } + } + } + + // For other steps + if (step !== 1 && step !== 2 && step !== 4) { + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.border = '1px solid #ef4444'; + } else { + field.style.border = ''; + } + }); + } + } + + return isValid; + } + + // Email validation function + function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + // Function to get form ID based on step + function getFormId(step) { + switch(step) { + case 1: return 'account-form'; + case 2: return 'personal-form'; + case 3: return 'contact-form'; + case 4: return 'payment-form'; + default: return ''; + } + } + + // Function to save step data to formData object + function saveStepData(step) { + switch(step) { + case 1: + formData.userDetails.email = document.getElementById('email').value; + formData.userDetails.password = document.getElementById('password').value; + break; + case 2: + formData.userDetails.firstName = document.getElementById('firstName').value; + formData.userDetails.lastName = document.getElementById('lastName').value; + formData.userDetails.age = document.getElementById('age').value; + formData.userDetails.dob = document.getElementById('dob').value; + formData.userDetails.address = document.getElementById('address').value; + formData.userDetails.city = document.getElementById('city').value; + formData.userDetails.state = document.getElementById('state').value; + formData.userDetails.country = document.getElementById('country').value; + break; + case 3: + formData.userDetails.phone = document.getElementById('phone').value; + formData.userDetails.skype = document.getElementById('skype').value; + break; + case 4: + const selectedPayment = document.querySelector('.payment-option.selected'); + if (selectedPayment) { + const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; + formData.userDetails.paymentMethod = paymentMethod; + + if (paymentMethod === 'payoneer') { + formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value; + } else if (paymentMethod === 'veem') { + formData.userDetails.veemAccount = document.getElementById('veemAccount').value; + } + } + break; + } + } + + // Function to select payment method + function selectPaymentMethod(method) { + // Remove selected class from all options + document.querySelectorAll('.payment-option').forEach(option => { + option.classList.remove('selected'); + }); + + // Hide all payment details + document.querySelectorAll('.payment-details').forEach(details => { + details.style.display = 'none'; + }); + + // Add selected class to clicked option + event.currentTarget.classList.add('selected'); + + // Show appropriate payment details + if (method === 'payoneer') { + document.getElementById('payoneer-details').style.display = 'block'; + } else if (method === 'veem') { + document.getElementById('veem-details').style.display = 'block'; + } + } + + // Function to generate a strong password + function generatePassword() { + const length = 12; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"; + let password = ""; + + // Ensure at least one of each required character type + password += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)]; + password += "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)]; + password += "0123456789"[Math.floor(Math.random() * 10)]; + password += "!@#$%^&*"[Math.floor(Math.random() * 8)]; + + // Fill the rest randomly + for (let i = password.length; i < length; i++) { + password += charset.charAt(Math.floor(Math.random() * charset.length)); + } + + // Shuffle the password + password = password.split('').sort(() => 0.5 - Math.random()).join(''); + + // Set the password field + document.getElementById('password').value = password; + document.getElementById('confirmPassword').value = password; + + // Trigger input events to update UI + document.getElementById('password').dispatchEvent(new Event('input')); + document.getElementById('confirmPassword').dispatchEvent(new Event('input')); + + // Update password strength indicator + updatePasswordStrength(password); + } + + // Function to toggle password visibility + function togglePasswordVisibility() { + const passwordField = document.getElementById('password'); + const confirmField = document.getElementById('confirmPassword'); + const toggleButton = document.querySelector('.password-toggle'); + + if (passwordField.type === 'password') { + passwordField.type = 'text'; + confirmField.type = 'text'; + toggleButton.textContent = 'Hide Password'; + } else { + passwordField.type = 'password'; + confirmField.type = 'password'; + toggleButton.textContent = 'Show Password'; + } + } + + // Function to update password strength indicator + function updatePasswordStrength(password) { + const strengthBar = document.getElementById('password-strength-bar'); + + // Reset classes + strengthBar.className = 'password-strength-bar'; + + if (password.length === 0) { + strengthBar.style.width = '0'; + } else if (password.length < 6) { + strengthBar.classList.add('password-weak'); + } else if (password.length < 10) { + strengthBar.classList.add('password-medium'); + } else { + // Check for complexity + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasNumbers = /\d/.test(password); + const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password); + + const complexityScore = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecial].filter(Boolean).length; + + if (complexityScore >= 3) { + strengthBar.classList.add('password-strong'); + } else { + strengthBar.classList.add('password-medium'); + } + } + } + + // Function to submit the form + async function submitForm() { + // Validate current step + if (!validateStep(currentStep)) { + return; + } + + // Save current step data + saveStepData(currentStep); + + // Show loading state + const submitBtn = document.querySelector('#payment-form .btn-primary'); + const originalText = submitBtn.textContent; + submitBtn.textContent = 'Processing...'; + submitBtn.disabled = true; + + try { + // Send data to server using fetch + // console.log('Submitting form data:', formData); + const response = await fetch('/api/v1/va/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + // Success - move to success step + // Mark current step as completed + document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => { + el.classList.add('completed'); + const stepNumber = el.querySelector('.step-number'); + stepNumber.textContent = '✓'; + stepNumber.style.backgroundColor = '#000000'; + stepNumber.style.borderColor = '#000000'; + stepNumber.style.color = 'white'; + }); + + nextStep(5); + } else { + // Handle server errors + alert('There was an error submitting your registration. Please try again.'); + } + } catch (error) { + // Handle network errors + console.error('Error:', error); + alert('There was a network error. Please check your connection and try again.'); + } finally { + // Restore button state + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } + } + + + // Initialize form interactions + // document.addEventListener('DOMContentLoaded', function() { + // // Password strength indicator + // document.getElementById('password').addEventListener('input', function() { + // updatePasswordStrength(this.value); + // }); + + // // Add input event listeners to handle label animation + // document.querySelectorAll('.input-field').forEach(input => { + // input.addEventListener('input', function() { + // if (this.value) { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // } else { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + + // input.addEventListener('focus', function() { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // }); + + // input.addEventListener('blur', function() { + // if (!this.value) { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + // }); + + // // Handle select field label animation + // document.querySelectorAll('.select-field').forEach(select => { + // select.addEventListener('change', function() { + // if (this.value) { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // } else { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + + // select.addEventListener('focus', function() { + // this.nextElementSibling.style.top = '4px'; + // this.nextElementSibling.style.fontSize = '12px'; + // this.nextElementSibling.style.transform = 'none'; + // }); + + // select.addEventListener('blur', function() { + // if (!this.value) { + // this.nextElementSibling.style.top = '50%'; + // this.nextElementSibling.style.fontSize = '14px'; + // this.nextElementSibling.style.transform = 'translateY(-50%)'; + // } + // }); + // }); + + // // Step navigation for desktop + // document.querySelectorAll('.step').forEach(step => { + // step.addEventListener('click', function() { + // const stepNum = parseInt(this.getAttribute('data-step')); + // if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + // goToStep(stepNum); + // } + // }); + // }); + + // // Step navigation for mobile + // document.querySelectorAll('.mobile-step').forEach(step => { + // step.addEventListener('click', function() { + // const stepNum = parseInt(this.getAttribute('data-step')); + // if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) { + // goToStep(stepNum); + // } + // }); + // }); + // }); \ No newline at end of file diff --git a/src/controllers/client/clientRefreshToken.controller.js b/src/controllers/client/clientRefreshToken.controller.js new file mode 100644 index 0000000..f705fbe --- /dev/null +++ b/src/controllers/client/clientRefreshToken.controller.js @@ -0,0 +1,65 @@ +import jwt from "jsonwebtoken"; +import { ClientModel } from "../../models/vaModel.js"; + +// Refresh Token VA +async function client_refresh_token_callback(req, res) { + try { + const { refreshToken } = req.body || {}; + + if (!refreshToken) { + return res.status(400).json({ + success: false, + message: "Refresh token missing", + }); + } + + // 🔍 Verify refresh token + jwt.verify( + refreshToken, + process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken", + async (err, decoded) => { + if (err) { + return res.status(403).json({ + success: false, + message: "Invalid or expired refresh token", + }); + } + + // Check if user still exists + const user = await ClientModel.findById(decoded.id); + if (!user) { + return res.status(404).json({ + success: false, + message: "User no longer exists", + }); + } + + // Generate a new access token + const newAccessToken = jwt.sign( + { + id: user._id, + email: user.userDetails.email, + name: user.userDetails.firstName, + }, + process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerAccessKey", + { expiresIn: "24h" } + ); + + await ClientModel.updateOne({ "userDetails.email": user.userDetails.email }, { + $set: { accessToken: newAccessToken } + }) + + return res.status(200).json({ + success: true, + message: "New access token generated", + accessToken: newAccessToken, + }); + } + ); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { client_refresh_token_callback }; \ No newline at end of file diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js new file mode 100644 index 0000000..f34de5d --- /dev/null +++ b/src/controllers/client/vaClient.controller.js @@ -0,0 +1,76 @@ +import VaClientRepository from "../../repository/vaClient.repository.js"; +import VaClientService from "../../services/vaClient.service.js"; +import logger from "../../config/logger.js"; + +const vaClientService = new VaClientService(new VaClientRepository()); + + +async function va_register_client(req, res) { + logger.info("ClientController - Register Client Hit ..."); + + try { + const payload = req.body || {}; + const { userDetails } = payload; + + if (!userDetails || !userDetails.email || !userDetails.name) { + return res.status(400).json({ + success: false, + message: "Missing required fields: name and email are required" + }); + } + + const created = await vaClientService.registerClient(payload); + + logger.info(`ClientController - Client registered successfully: ${userDetails.email}`); + return res.status(201).json({ + success: true, + data: created, + message: "Client registered successfully" + }); + } catch (error) { + logger.error(`ClientController - Register error: ${error.message}`); + + if (error.message === "User already exists with this email") { + return res.status(409).json({ + success: false, + message: error.message + }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + +// Additional controller methods can be added here +async function get_client_by_email(req, res) { + try { + const { email } = req.params; + const client = await vaClientService.getClientByEmail(email); + + if (!client) { + return res.status(404).json({ + success: false, + message: "Client not found" + }); + } + + return res.status(200).json({ + success: true, + data: client + }); + } catch (error) { + logger.error(`ClientController - Get client error: ${error.message}`); + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + +export { + va_register_client, + get_client_by_email +}; \ No newline at end of file diff --git a/src/controllers/client/vaClientLogin.controller.js b/src/controllers/client/vaClientLogin.controller.js new file mode 100644 index 0000000..384c132 --- /dev/null +++ b/src/controllers/client/vaClientLogin.controller.js @@ -0,0 +1,79 @@ +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { ClientModel } from "../../models/vaModel.js"; + +// Login VA +async function va_login_client(req, res) { + try { + // const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body || {}; + const { userDetails } = payload; + + if (!userDetails || !userDetails.email || !userDetails.password) { + return res.status(400).json({ success: false, message: "Please provide email and password" }); + } + + // Find user by email + const user = await ClientModel.findOne({ "userDetails.email": userDetails.email }); + if (!user) { + return res.status(404).json({ success: false, message: "Invalid credentials" }); + } + + // const saltRounds = 10; + // const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds); + + // Compare password with stored hash + const isMatch = await bcrypt.compare( + userDetails.password, + user.userDetails.password + ); + + if (!isMatch) { + return res.status(401).json({ success: false, message: "Invalid password" }); + } + + // Generate JWT accessToken with 24h expiry + const accessToken = jwt.sign( + { + id: user._id, + email: user.userDetails.email, + name: user.userDetails.firstName, + }, + process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey", + { expiresIn: "24h" } + ); + + // Generate JWT refreshToken with 24h expiry + const refreshToken = jwt.sign( + { + id: user._id, + email: user.userDetails.email, + name: user.userDetails.firstName, + }, + process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken", + { expiresIn: "30d" } + ); + + await ClientModel.updateOne({ "userDetails.email": userDetails.email }, { + $set: { accessToken, refreshToken } + }) + + // Return accessToken and user info + return res.status(200).json({ + success: true, + message: "Login Successfull", + accessToken, + refreshToken, + user: { + id: user._id, + name: user.userDetails.firstName, + email: user.userDetails.email, + }, + }); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_login_client }; \ No newline at end of file diff --git a/src/controllers/client/vaClientPasswordReset.controllers.js b/src/controllers/client/vaClientPasswordReset.controllers.js new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/va/vaPasswordReset.controllers.js b/src/controllers/va/vaPasswordReset.controllers.js new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js index 9354858..61352fc 100644 --- a/src/controllers/va/vaProfile/vaRegister.controller.js +++ b/src/controllers/va/vaProfile/vaRegister.controller.js @@ -4,17 +4,13 @@ import { VAModel } from "../../../models/vaModel.js"; // Register a new VA async function va_register(req, res) { try { - const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body|| {}; const { userDetails } = payload; if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) { return res.status(400).json({ success: false, message: "Missing required fields" }); } - if (userDetails.password !== userDetails.cPassword) { - return res.status(400).json({ success: false, message: "password not matching" }); - } - // ✅ Check if user already exists by email const existingUser = await VAModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() }); if (existingUser) { @@ -33,11 +29,15 @@ async function va_register(req, res) { dob: userDetails.dob || null, age: userDetails.age || null, address: userDetails.address || null, + city: userDetails.city || null, + state: userDetails.state || null, + country: userDetails.country || null, password: hashedPassword, - bankName: userDetails.bankName || null, - accountHolderName: userDetails.accountHolderName || null, - accountNumber: userDetails.accountNumber || null, - routingNumber: userDetails.routingNumber || null, + paymentMethod: userDetails.paymentMethod || null, + payoneerAccount: userDetails.payoneerAccount || null, + veemAccount: userDetails.veemAccount || null, + phone: userDetails.phone || null, + skype: userDetails.skype || null, }, tracking: payload.tracking || undefined, clientIds: payload.clientIds || [], diff --git a/src/controllers/va/vaProjects/vaProjectLogList.controller.js b/src/controllers/va/vaProjects/vaProjectLogList.controller.js index 13e9f72..be7b7fb 100644 --- a/src/controllers/va/vaProjects/vaProjectLogList.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLogList.controller.js @@ -1,3 +1,4 @@ +import jwt from "jsonwebtoken"; import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js"; function formatHMS(ms) { @@ -11,50 +12,63 @@ function formatHMS(ms) { async function va_projects_log_list_function(req, res) { try { const { projectId, from, to } = req.params; + const accessToken = req.headers.token; - if (!projectId) { - return res.status(400).json({ success: false, message: "Missing projectId" }); + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing token" }); } - // 📅 Date filtering + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + if (!decoded || !decoded.email) { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + const vaEmail = decoded.email; + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } + + const vaId = vaUser._id; + + // Date filtering const now = new Date(); const fromDate = from && from !== "undefined" ? new Date(from) : new Date(now.setHours(0, 0, 0, 0)); const toDate = to && to !== "undefined" ? new Date(to) : new Date(); toDate.setHours(23, 59, 59, 999); - // 🧩 Fetch Project - const project = await ProjectModel.findById(projectId).populate("vaAssigned", { - "userDetails.firstName": 1, - "userDetails.lastName": 1, - "userDetails.email": 1, - status: 1 - }); + let projectList = []; + if (projectId) { + const project = await ProjectModel.findById(projectId); + if (!project) { + return res.status(404).json({ success: false, message: "Project not found" }); + } - if (!project) { - return res.status(404).json({ success: false, message: "Project not found" }); + // Check if VA is assigned to this project + const isAssigned = project.vaAssigned.some(id => id.toString() === vaId.toString()); + if (!isAssigned) { + return res.status(403).json({ success: false, message: "VA not assigned to this project" }); + } + + projectList = [project]; + } else { + projectList = await ProjectModel.find({ vaAssigned: vaId }); } - const vaList = project.vaAssigned || []; - if (!vaList.length) { - return res.status(200).json({ success: true, message: "No VAs assigned to this project", data: [] }); + if (!projectList.length) { + return res.status(200).json({ success: true, message: "No assigned projects found", data: [] }); } - let totalProjectWorkMs = 0; - let activeCount = 0; + const finalReport = []; - const vaReport = []; - - // 🔁 Loop through each VA assigned to the project - for (const va of vaList) { - const vaId = va._id; - const vaEmail = va.userDetails?.email; - - // 🔎 Get all login/logout logs for this VA in the date range + for (const project of projectList) { const logs = await LoggedModel.find({ vaId, - projectId, + projectId: project._id, time: { $gte: fromDate, $lte: toDate } - }).sort({ time: 1 }).lean(); + }) + .sort({ time: 1 }) + .lean(); let totalWorkMs = 0; let currentLogin = null; @@ -68,46 +82,40 @@ async function va_projects_log_list_function(req, res) { } } - // if still logged in (no logout yet) — count up to now + // still logged in → count till now if (currentLogin) { totalWorkMs += new Date() - new Date(currentLogin.time); } - totalProjectWorkMs += totalWorkMs; - - // 🟢 Determine Active / Away status const lastLog = logs[logs.length - 1]; - let activityStatus = "AWAY"; - if (lastLog?.loggedType === "LOGIN") { - activityStatus = "ACTIVE"; - activeCount++; - } + const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY"; - vaReport.push({ - vaId, - name: `${va.userDetails?.firstName || ""} ${va.userDetails?.lastName || ""}`.trim(), - email: vaEmail, - status: va.status, + finalReport.push({ + projectId: project._id, + projectName: project.name, activityStatus, totalWorkTime: formatHMS(totalWorkMs), totalWorkMs, + totalLogs: logs.length }); } - // 📊 Prepare Summary - const totalVAs = vaReport.length; - const awayCount = totalVAs - activeCount; + // Summary for current VA + const totalProjects = finalReport.length; + const activeProjects = finalReport.filter(r => r.activityStatus === "ACTIVE").length; + const awayProjects = totalProjects - activeProjects; + const totalMs = finalReport.reduce((acc, r) => acc + r.totalWorkMs, 0); res.status(200).json({ success: true, - projectId, - projectName: project.name, - totalVAs, - activeVAs: activeCount, - awayVAs: awayCount, - totalProjectWorkTime: formatHMS(totalProjectWorkMs), - totalProjectWorkMs, - data: vaReport + vaId, + vaName: `${vaUser.userDetails.firstName} ${vaUser.userDetails.lastName}`, + totalProjects, + activeProjects, + awayProjects, + totalWorkTime: formatHMS(totalMs), + totalWorkMs: totalMs, + data: finalReport }); } catch (err) { diff --git a/src/controllers/va/vaUsername.controller.js b/src/controllers/va/vaUsername.controller.js index 72af821..038fe6e 100644 --- a/src/controllers/va/vaUsername.controller.js +++ b/src/controllers/va/vaUsername.controller.js @@ -1,89 +1,109 @@ -import { VAModel, LoggedModel } from "../../models/vaModel.js"; +import { VAModel, ProjectModel, LoggedModel } from "../../models/vaModel.js"; // Getting VA details by username async function va_username_callback(req, res) { try { const { userName } = req.params; if (!userName) { - return res.status(400).json({ success: false, message: "Missing userName param" }); + return res.status(400).json({ success: false, message: "Missing username" }); } - // Find VA by common identifier fields (id, email, username, first/last name) - const va = await VAModel.findOne({ + // Find VA by firstName or lastName or email (depending on how you store username) + const vaUser = await VAModel.findOne({ $or: [ - { _id: userName }, { "userDetails.email": userName }, - { "userDetails.userName": userName }, { "userDetails.firstName": userName }, { "userDetails.lastName": userName }, - { "userDetails.fullName": userName } - ] - }).lean().exec(); + ], + }) + .populate("projectIds", "name totalLoggedHours") + .lean(); - if (!va) { + if (!vaUser) { return res.status(404).json({ success: false, message: "VA not found" }); } - // Latest log entry to determine active state - const lastLog = await LoggedModel.findOne({ vaId: va._id }).sort({ time: -1 }).lean().exec(); - const active = lastLog ? String(lastLog.loggedType).toUpperCase() === "LOGIN" : false; + const vaId = vaUser._id; - // Fetch all logs for this VA (ordered ascending) to compute logged hours - const logs = await LoggedModel.find({ vaId: va._id }).sort({ time: 1 }).lean().exec(); + // Get all logs for this VA + const logs = await LoggedModel.find({ vaId }).sort({ time: 1 }).populate("projectId", "name").lean(); - let totalSeconds = 0; + if (!logs.length) { + return res.status(200).json({ + success: true, + message: "No logs found for this VA", + data: { + va: vaUser, + totalWorkTime: "00h 00m 00s", + totalWorkMs: 0, + projects: [], + }, + }); + } - if (logs.length) { - // If logs have an explicit duration field, sum it - if (logs[0].duration !== undefined) { - totalSeconds = logs.reduce((sum, l) => sum + (Number(l.duration) || 0), 0); - } else { - // Otherwise compute from LOGIN/LOGOUT pairs - let lastLogin = null; - for (const l of logs) { - const t = l.time ? new Date(l.time) : null; - const type = String(l.loggedType || "").toUpperCase(); + // Compute time spent on each project + let totalWorkMs = 0; + const projectWork = {}; - if (type === "LOGIN" && t) { - lastLogin = t; - } else if (type === "LOGOUT" && t && lastLogin) { - const diff = (t.getTime() - lastLogin.getTime()) / 1000; - if (diff > 0) totalSeconds += diff; - lastLogin = null; - } - } - // If currently logged in, add time since last login - if (lastLogin) { - totalSeconds += (Date.now() - lastLogin.getTime()) / 1000; + let currentLogin = null; + let currentProject = null; + + for (const log of logs) { + const time = new Date(log.time); + if (log.loggedType === "LOGIN") { + currentLogin = time; + currentProject = log.projectId?._id?.toString() || null; + } else if (log.loggedType === "LOGOUT" && currentLogin) { + const diff = time - currentLogin; + totalWorkMs += diff; + if (currentProject) { + projectWork[currentProject] = (projectWork[currentProject] || 0) + diff; } + currentLogin = null; + currentProject = null; } } - const hours = Math.floor(totalSeconds / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = Math.floor(totalSeconds % 60); - const totalHours = `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; + // Format helper + const formatHMS = (ms) => { + const totalSeconds = Math.floor(ms / 1000); + const h = Math.floor(totalSeconds / 3600); + const m = Math.floor((totalSeconds % 3600) / 60); + const s = totalSeconds % 60; + return `${h}h ${m}m ${s}s`; + }; - // Project / client info (fall back to empty array) - const projects = va.clientIds || va.projects || []; - - const name = `${va.userDetails?.firstName || ""}`.trim() + (va.userDetails?.lastName ? ` ${va.userDetails.lastName}` : ""); - const email = va.userDetails?.email || null; + // Build per-project breakdown + const projects = await Promise.all( + Object.entries(projectWork).map(async ([projectId, ms]) => { + const project = await ProjectModel.findById(projectId).lean(); + return { + projectId, + projectName: project?.name || "Unknown Project", + totalWorkMs: ms, + totalWorkTime: formatHMS(ms), + }; + }) + ); + // Final Response return res.status(200).json({ success: true, - data: { - id: va._id, - name: name || null, - email, - active, - totalHours, - lastLog: lastLog || null, - projects - } + va: { + id: vaUser._id, + name: `${vaUser.userDetails.firstName || ""} ${vaUser.userDetails.lastName || ""}`.trim(), + email: vaUser.userDetails.email, + status: vaUser.status, + schedule: vaUser.schedule, + tracking: vaUser.tracking, + totalAssignedProjects: vaUser.projectIds?.length || 0, + }, + totalWorkMs, + totalWorkTime: formatHMS(totalWorkMs), + projects, }); } catch (error) { - console.error("va_username_callback error:", error); + console.error("Error in va_username_callback:", error); return res.status(500).json({ success: false, message: "Internal Server Error" }); } } diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 8c32d0d..e2426b8 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -25,19 +25,31 @@ const userDetailsSchema = new Schema({ address: { type: String, }, + city: { + type: String, + }, + state: { + type: String, + }, + country: { + type: String, + }, password: { type: String, }, - bankName: { + paymentMethod: { type: String, }, - accountHolderName: { + payoneerAccount: { type: String, }, - accountNumber: { + veemAccount: { type: String, }, - routingNumber: { + phone: { + type: String, + }, + skype: { type: String, }, }); @@ -61,6 +73,9 @@ const projectSchema = new Schema({ type: String, required: true, }, + hourlyRate: { + type: String, + }, totalLoggedHours: { type: Number, default: 0 @@ -77,16 +92,93 @@ const projectSchema = new Schema({ ], }); +/* ---------------------- Client Details Schema ---------------------- */ +const clientDetailsSchema = new Schema({ + name: { + type: String, + }, + email: { + type: String, + required: true, + unique: true, + }, + address: { + type: String, + }, + city: { + type: String, + }, + state: { + type: String, + }, + zip: { + type: String, + }, + billingName: { + type: String, + }, + billingEmail: { + type: String, + }, + billingAddress: { + type: String, + }, + billingCity: { + type: String, + }, + billingState: { + type: String, + }, + billingZip: { + type: String, + }, + position: { + type: String, + }, + signatoryName: { + type: String, + }, + signatoryEmail: { + type: String, + }, + paymentMethod: { + type: String, + }, + bankName: { + type: String, + }, + accountNumber: { + type: String, + }, + routingNumber: { + type: String, + }, + accountType: { + type: String, + }, + accountHolderName: { + type: String, + }, + cardNumber: { + type: String, + }, + expiryDate: { + type: String, + }, + securityCode: { + type: String, + }, + cardZip: { + type: String, + }, + password: { + type: String, + }, +}); + /* ---------------------- Client Schema ---------------------- */ const clientSchema = new Schema({ - userDetails: { - name: { - type: String, - }, - email: { - type: String, - }, - }, + userDetails: clientDetailsSchema, vaIds: [ { type: Types.ObjectId, @@ -99,9 +191,14 @@ const clientSchema = new Schema({ ref: "Project", }, ], - totalLoggedHours: { - type: Number, + accessToken: { + type: String, + default: null }, + refreshToken: { + type: String, + default: null + } }); @@ -218,4 +315,4 @@ const ProjectModel = model("Project", projectSchema); const LoggedModel = model("TrackLog", loggedSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel }; +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel }; \ No newline at end of file diff --git a/src/repository/vaClient.repository.js b/src/repository/vaClient.repository.js new file mode 100644 index 0000000..d087d4c --- /dev/null +++ b/src/repository/vaClient.repository.js @@ -0,0 +1,55 @@ +import { ClientModel } from "../models/vaModel.js"; + +class VaClientRepository { + async create(data) { + try { + const newClient = new ClientModel({ ...data }); + const response = await newClient.save(); + return response; + } catch (error) { + throw error; + } + } + + async findByEmail(email) { + try { + const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() }); + return response; + } catch (error) { + throw error; + } + } + + async findById(id) { + try { + const response = await ClientModel.findById(id); + return response; + } catch (error) { + throw error; + } + } + + async update(id, data) { + try { + const response = await ClientModel.findByIdAndUpdate( + id, + { $set: { ...data } }, + { new: true } + ); + return response; + } catch (error) { + throw error; + } + } + + async delete(id) { + try { + const response = await ClientModel.findByIdAndDelete(id); + return response; + } catch (error) { + throw error; + } + } +} + +export default VaClientRepository; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js new file mode 100644 index 0000000..ef7b746 --- /dev/null +++ b/src/routes/v1/client.routes.js @@ -0,0 +1,29 @@ +import express from "express"; +const clientRoutes = express.Router(); + +import { va_register_client } from "../../controllers/client/vaClient.controller.js"; +import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js"; +import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; +import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js"; +import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.controller.js"; + +// clients routes +clientRoutes.post("/register", va_register_client); // Register a new VA +clientRoutes.post("/login", va_login_client); // Login Client +clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token + + +clientRoutes.get("/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC) +clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) + + +// // API endpoint (for frontend to call) +// clientRoutes.post("/forgot-password", va_client_request_password_reset); + +// // EJS rendering routes +// clientRoutes.get("/reset-form", va_client_render_reset_form); +// clientRoutes.post("/reset", va_client_process_reset); + + + +export default clientRoutes; \ No newline at end of file diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index e9747f7..0b30a88 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -1,6 +1,7 @@ import path from 'path'; import authRoutes from './auth.routes.js'; import vaRoutes from './va.routes.js'; +import clientRoutes from './client.routes.js'; import express from "express"; @@ -19,6 +20,7 @@ v1Routes.get("/auth", function(req, res) { v1Routes.use("/auth_callback", authRoutes); v1Routes.use("/va", vaRoutes); +v1Routes.use("/client", clientRoutes); v1Routes.get("/", function(req, res) { res.json({"connected": true}) diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 72076b4..34cc5b2 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -11,10 +11,7 @@ import { va_logout_callback } from "../../controllers/va/vaAuth/vaLogout.control import { va_filter_search } from "../../controllers/va/vaFilterSearch.controller.js"; import { va_log_callback } from "../../controllers/va/vaLog/vaLog.controller.js"; import { va_callback } from "../../controllers/va/vaLog/vaListLog.controller.js"; -import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js"; -import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; import { va_project_log_callback } from "../../controllers/va/vaProjects/vaProjectLog.controller.js"; -import { va_projects_callback } from "../../controllers/va/vaProjects/vaProjects.controller.js"; import { authValidator } from "../../middlewares/auth.middleware.js"; import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrackWork.controller.js"; import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js"; @@ -31,22 +28,23 @@ vaRoutes.post("/register", va_register); // Register a new VA vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile -vaRoutes.get("/:userName", va_username_callback); // VA Username Callback -// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 + + +vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC) + + + + +// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC) vaRoutes.get("/search", va_filter_search); -vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID -vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback - - - -// 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); // GET Windows Tab Logged Data for VA IN Between Login and LOGOUT vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function); +// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA +vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function); vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project @@ -56,4 +54,13 @@ vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); / vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report // VA filter search: supports query params `name`, `email`, `from`, `to` + +// // API endpoint (for frontend to call) +// vaRoutes.post("/forgot-password", va_request_password_reset); + +// // EJS rendering routes +// vaRoutes.get("/reset-form", va_render_reset_form); +// vaRoutes.post("/reset", va_process_reset); + + export default vaRoutes; \ No newline at end of file diff --git a/src/server.js b/src/server.js index e5016b5..7907508 100644 --- a/src/server.js +++ b/src/server.js @@ -47,6 +47,10 @@ app.get('/varegister', (req, res) => { res.render('va_register', { title: "VA Registration" }); }); +app.get('/clientregister', (req, res) => { + res.render('va_client_register', { title: "Client Registration" }); +}); + app.get('/health', (req, res) => { res.status(201).send({ status: "Server is healthy" }); }) diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js new file mode 100644 index 0000000..e54f0ec --- /dev/null +++ b/src/services/vaClient.service.js @@ -0,0 +1,107 @@ +import logger from "../config/logger.js"; + +class VaClientService { + constructor(vaClientRepository) { + this.clientRepository = vaClientRepository; + } + + async registerClient(data) { + try { + // Check if user already exists + const existingUser = await this.clientRepository.findByEmail(data.userDetails.email); + if (existingUser) { + throw new Error("User already exists with this email"); + } + + // Sanitize and prepare data + const clientData = this.sanitizeClientData(data); + + const response = await this.clientRepository.create(clientData); + return response; + } catch (error) { + logger.error("Client Service - registerClient error: " + error.message); + throw error; + } + } + + async getClientByEmail(email) { + try { + const response = await this.clientRepository.findByEmail(email); + return response; + } catch (error) { + logger.error("Client Service - getClientByEmail error: " + error.message); + throw error; + } + } + + async getClientById(id) { + try { + const response = await this.clientRepository.findById(id); + if (!response) throw new Error("Client not found"); + return response; + } catch (error) { + logger.error("Client Service - getClientById error: " + error.message); + throw error; + } + } + + async updateClient(id, data) { + try { + const response = await this.clientRepository.update(id, data); + if (!response) throw new Error("Client not found"); + return response; + } catch (error) { + logger.error("Client Service - updateClient error: " + error.message); + throw error; + } + } + + async deleteClient(id) { + try { + const response = await this.clientRepository.delete(id); + return response; + } catch (error) { + logger.error("Client Service - deleteClient error: " + error.message); + throw error; + } + } + + // Helper method to sanitize data + sanitizeClientData(payload) { + const { userDetails } = payload; + + return { + userDetails: { + name: userDetails.name, + email: userDetails.email.toLowerCase(), + address: userDetails.address || null, + city: userDetails.city || null, + state: userDetails.state || null, + zip: userDetails.zip || null, + billingName: userDetails.billingName || null, + billingAddress: userDetails.billingAddress || null, + billingCity: userDetails.billingCity || null, + billingState: userDetails.billingState || null, + billingZip: userDetails.billingZip || null, + billingEmail: userDetails.billingEmail || null, + position: userDetails.position || null, + signatoryName: userDetails.signatoryName || null, + signatoryEmail: userDetails.signatoryEmail || null, + paymentMethod: userDetails.paymentMethod || null, + bankName: userDetails.bankName || null, + accountNumber: userDetails.accountNumber || null, + routingNumber: userDetails.routingNumber || null, + accountType: userDetails.accountType || null, + accountHolderName: userDetails.accountHolderName || null, + cardNumber: userDetails.cardNumber || null, + expiryDate: userDetails.expiryDate || null, + securityCode: userDetails.securityCode || null, + cardZip: userDetails.cardZip || null, + }, + vaIds: payload.vaIds || [], + projectIds: payload.projectIds || [], + }; + } +} + +export default VaClientService; \ No newline at end of file diff --git a/views/va_client_register.ejs b/views/va_client_register.ejs new file mode 100644 index 0000000..b9c7d30 --- /dev/null +++ b/views/va_client_register.ejs @@ -0,0 +1,343 @@ + + + + + + Client Registration + + + + + + +
+ + + + +
+ +
+
    +
  1. +
    1
    + +
    Create Account
    +
  2. +
  3. +
    2
    +
    Business Info
    +
  4. +
  5. +
    3
    +
    Billing
    +
  6. +
  7. +
    4
    +
    Signatory
    +
  8. +
  9. +
    5
    +
    Payment
    +
  10. +
  11. +
    6
    +
    Done
    +
  12. +
+
+ + +
+

Client Registration

+
+ +
+

Create Your Account

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+ + +
+

Business Information

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + + +
Detecting your location...
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+

Billing Address

+
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + + +
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+

Authorized Signatory

+

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

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+

Payment Details

+
+
+

Credit or Debit Card

+

Pay securely with your card

+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+

Bank Account (ACR)

+

Direct bank transfer

+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+
+
✓
+

Registration Complete!

+

Your account has been successfully created. You will receive a confirmation email shortly.

+
+
+
+
+
+
+ + + diff --git a/views/va_register.ejs b/views/va_register.ejs index 6228575..ade74f7 100644 --- a/views/va_register.ejs +++ b/views/va_register.ejs @@ -3,13 +3,12 @@ - VA Registration + Virtual Assistant Registration - - + -
-
-
-
-
-
- - -
- Registration + +
+ + + +
+ + +
+ +
+
    +
  1. +
    1
    +
    Account
    +
  2. +
  3. +
    2
    +
    Personal
    +
  4. +
  5. +
    3
    +
    Contact
    +
  6. +
  7. +
    4
    +
    Payment
    +
  8. +
  9. +
    5
    +
    Done
    +
  10. +
+
+ + +
+

Virtual Assistant Registration

+
+ +
+

Account Setup

+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+
+ + +
+
Use at least 8 characters with a mix of letters, numbers, and symbols
+
+ +
+
+ + +
+ +
+ +
+
+ +
+
+
+ + +
+

Personal Information

+ + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ + +
Loading states...
+
+
+ +
+ + +
+
+
+ + +
+

Contact Information

+
+
+ +
+ + + + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+

Payment Details

+
+
+ +
+

Payoneer

+

Receive payments via Payoneer

+
+
+
+
+ + +
+
+
+ +
+

Veem

+

Receive payments via Veem

+
+
+
+
+ + +
+
+
+
+ +
+ + +
+
+
+ + +
+
+
✓
+

Registration Complete!

+

Your Virtual Assistant account has been successfully created..

+
+
+
-
-
- +