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
+
+ Create Account
+
+ -
+
2
+ Business Info
+
+ -
+
3
+ Billing
+
+ -
+
4
+ Signatory
+
+ -
+
5
+ Payment
+
+ -
+
6
+ Done
+
+
+
+
+
+
+
+
+
+
+
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
-
-
+
-
-
-
-
-
-
-
-
-
-

+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
1
+ Account
+
+ -
+
2
+ Personal
+
+ -
+
3
+ Contact
+
+ -
+
4
+ Payment
+
+ -
+
5
+ Done
+
+
+
+
+
+
-
-
-
+