Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware
This commit is contained in:
commit
47f8d96a02
|
|
@ -45,8 +45,11 @@
|
||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.26.4",
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
"terser-webpack-plugin": "^5.3.11",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"webpack": "^5.97.1",
|
"webpack": "^5.97.1",
|
||||||
"webpack-cli": "^6.0.1",
|
"webpack-cli": "^6.0.1",
|
||||||
|
|
@ -1828,9 +1831,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
|
@ -2546,6 +2549,44 @@
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"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": {
|
"node_modules/aws-sign2": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||||
|
|
@ -3015,9 +3056,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001692",
|
"version": "1.0.30001754",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
||||||
"integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==",
|
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -3973,9 +4014,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.0",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
|
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -4726,6 +4767,20 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
|
@ -6604,6 +6659,25 @@
|
||||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
|
@ -6782,6 +6856,16 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/nth-check": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
|
||||||
|
|
@ -7244,6 +7328,42 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
|
@ -8423,6 +8543,16 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/source-map-support": {
|
||||||
"version": "0.5.21",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
"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"
|
"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": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
body {
|
||||||
background-color: #212529;
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-registration {
|
.container {
|
||||||
border-radius: 15px;
|
display: flex;
|
||||||
}
|
height: 100vh;
|
||||||
|
|
||||||
.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;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.form-outline .form-control.not-required:placeholder-shown + .form-label,
|
|
||||||
.form-outline .form-control.not-required:empty + .form-label {
|
/* Sidebar Styles */
|
||||||
top: 6px;
|
.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,
|
.logo {
|
||||||
.form-outline .form-control:valid + .form-label,
|
display: flex;
|
||||||
.form-outline .form-control.not-required:not(:placeholder-shown) + .form-label,
|
justify-content: center;
|
||||||
.form-outline .form-control.not-required:focus + .form-label {
|
margin-bottom: 32px;
|
||||||
top: -17px;
|
}
|
||||||
|
.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]) {
|
.form-title-sec {
|
||||||
font-size: 1rem;
|
font-size: 24px;
|
||||||
line-height: 2.15;
|
font-weight: 700;
|
||||||
padding-left: .75em;
|
color: #000;
|
||||||
padding-right: .75em;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
@ -1,46 +1,733 @@
|
||||||
// Example: Basic client-side validation or UX improvements
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
const form = document.querySelector("form");
|
|
||||||
|
|
||||||
form.addEventListener("submit", async (e) => {
|
// Form data object
|
||||||
e.preventDefault();
|
const formData = {
|
||||||
const password = document.getElementById("password").value;
|
userDetails: {}
|
||||||
const cPassword = document.getElementById("cPassword").value;
|
};
|
||||||
|
|
||||||
if (password !== cPassword) {
|
// Current step tracking
|
||||||
alert("Passwords do not match!");
|
let currentStep = 1;
|
||||||
|
|
||||||
|
// 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' }
|
||||||
|
];
|
||||||
|
|
||||||
|
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' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the form
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Populate country dropdown
|
||||||
|
const countrySelect = document.getElementById('country');
|
||||||
|
countrySelect.innerHTML = '<option value=""></option>';
|
||||||
|
|
||||||
|
countries.forEach(country => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = country.code;
|
||||||
|
option.textContent = country.name;
|
||||||
|
countrySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listener for country change
|
||||||
|
countrySelect.addEventListener('change', function() {
|
||||||
|
const countryCode = this.value;
|
||||||
|
const stateSelect = document.getElementById('state');
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
document.getElementById('state-loading').style.display = 'block';
|
||||||
|
|
||||||
|
// Clear current states
|
||||||
|
stateSelect.innerHTML = '<option value=""></option>';
|
||||||
|
|
||||||
|
// Simulate API delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (countryCode && states[countryCode]) {
|
||||||
|
states[countryCode].forEach(state => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = state.code;
|
||||||
|
option.textContent = state.name;
|
||||||
|
stateSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide loading
|
||||||
|
document.getElementById('state-loading').style.display = 'none';
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Password strength indicator
|
||||||
|
document.getElementById('password').addEventListener('input', function() {
|
||||||
|
updatePasswordStrength(this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add input event listeners to handle label animation
|
||||||
|
document.querySelectorAll('.input-field').forEach(input => {
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
if (this.value) {
|
||||||
|
this.nextElementSibling.style.top = '4px';
|
||||||
|
this.nextElementSibling.style.fontSize = '12px';
|
||||||
|
this.nextElementSibling.style.transform = 'none';
|
||||||
|
} else {
|
||||||
|
this.nextElementSibling.style.top = '50%';
|
||||||
|
this.nextElementSibling.style.fontSize = '14px';
|
||||||
|
this.nextElementSibling.style.transform = 'translateY(-50%)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('focus', function() {
|
||||||
|
this.nextElementSibling.style.top = '4px';
|
||||||
|
this.nextElementSibling.style.fontSize = '12px';
|
||||||
|
this.nextElementSibling.style.transform = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
if (!this.value) {
|
||||||
|
this.nextElementSibling.style.top = '50%';
|
||||||
|
this.nextElementSibling.style.fontSize = '14px';
|
||||||
|
this.nextElementSibling.style.transform = 'translateY(-50%)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle select field label animation
|
||||||
|
document.querySelectorAll('.select-field').forEach(select => {
|
||||||
|
select.addEventListener('change', function() {
|
||||||
|
if (this.value) {
|
||||||
|
this.nextElementSibling.style.top = '4px';
|
||||||
|
this.nextElementSibling.style.fontSize = '12px';
|
||||||
|
this.nextElementSibling.style.transform = 'none';
|
||||||
|
} else {
|
||||||
|
this.nextElementSibling.style.top = '50%';
|
||||||
|
this.nextElementSibling.style.fontSize = '14px';
|
||||||
|
this.nextElementSibling.style.transform = 'translateY(-50%)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
select.addEventListener('focus', function() {
|
||||||
|
this.nextElementSibling.style.top = '4px';
|
||||||
|
this.nextElementSibling.style.fontSize = '12px';
|
||||||
|
this.nextElementSibling.style.transform = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
select.addEventListener('blur', function() {
|
||||||
|
if (!this.value) {
|
||||||
|
this.nextElementSibling.style.top = '50%';
|
||||||
|
this.nextElementSibling.style.fontSize = '14px';
|
||||||
|
this.nextElementSibling.style.transform = 'translateY(-50%)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step navigation for desktop
|
||||||
|
document.querySelectorAll('.step').forEach(step => {
|
||||||
|
step.addEventListener('click', function() {
|
||||||
|
const stepNum = parseInt(this.getAttribute('data-step'));
|
||||||
|
if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) {
|
||||||
|
goToStep(stepNum);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step navigation for mobile
|
||||||
|
document.querySelectorAll('.mobile-step').forEach(step => {
|
||||||
|
step.addEventListener('click', function() {
|
||||||
|
const stepNum = parseInt(this.getAttribute('data-step'));
|
||||||
|
if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) {
|
||||||
|
goToStep(stepNum);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit();
|
// Hide current step
|
||||||
});
|
document.getElementById(`step-${currentStep}`).classList.remove('active');
|
||||||
|
document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
|
||||||
|
el.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit() {
|
document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => {
|
||||||
|
el.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
// Example: collect form data manually
|
// Show target step
|
||||||
const form = document.querySelector("form");
|
document.getElementById(`step-${step}`).classList.add('active');
|
||||||
const formData = new FormData(form);
|
document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => {
|
||||||
const payload = { userDetails: {} };
|
el.classList.add('active');
|
||||||
|
});
|
||||||
|
|
||||||
formData.forEach((value, key) => {
|
document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => {
|
||||||
// handle nested names like userDetails[firstName]
|
el.classList.add('active');
|
||||||
if (key.startsWith("userDetails[")) {
|
});
|
||||||
const innerKey = key.match(/userDetails\[(.*?)\]/)[1];
|
|
||||||
payload.userDetails[innerKey] = value;
|
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 {
|
} else {
|
||||||
payload[key] = value;
|
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 = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -4,17 +4,13 @@ import { VAModel } from "../../../models/vaModel.js";
|
||||||
// Register a new VA
|
// Register a new VA
|
||||||
async function va_register(req, res) {
|
async function va_register(req, res) {
|
||||||
try {
|
try {
|
||||||
const payload = req.body ? JSON.parse(req.body) : {};
|
const payload = req.body|| {};
|
||||||
const { userDetails } = payload;
|
const { userDetails } = payload;
|
||||||
|
|
||||||
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) {
|
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) {
|
||||||
return res.status(400).json({ success: false, message: "Missing required fields" });
|
return res.status(400).json({ success: false, message: "Missing required fields" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userDetails.password !== userDetails.cPassword) {
|
|
||||||
return res.status(400).json({ success: false, message: "password not matching" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Check if user already exists by email
|
// ✅ Check if user already exists by email
|
||||||
const existingUser = await VAModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() });
|
const existingUser = await VAModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() });
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
|
|
@ -33,11 +29,15 @@ async function va_register(req, res) {
|
||||||
dob: userDetails.dob || null,
|
dob: userDetails.dob || null,
|
||||||
age: userDetails.age || null,
|
age: userDetails.age || null,
|
||||||
address: userDetails.address || null,
|
address: userDetails.address || null,
|
||||||
|
city: userDetails.city || null,
|
||||||
|
state: userDetails.state || null,
|
||||||
|
country: userDetails.country || null,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
bankName: userDetails.bankName || null,
|
paymentMethod: userDetails.paymentMethod || null,
|
||||||
accountHolderName: userDetails.accountHolderName || null,
|
payoneerAccount: userDetails.payoneerAccount || null,
|
||||||
accountNumber: userDetails.accountNumber || null,
|
veemAccount: userDetails.veemAccount || null,
|
||||||
routingNumber: userDetails.routingNumber || null,
|
phone: userDetails.phone || null,
|
||||||
|
skype: userDetails.skype || null,
|
||||||
},
|
},
|
||||||
tracking: payload.tracking || undefined,
|
tracking: payload.tracking || undefined,
|
||||||
clientIds: payload.clientIds || [],
|
clientIds: payload.clientIds || [],
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
|
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
function formatHMS(ms) {
|
function formatHMS(ms) {
|
||||||
|
|
@ -11,50 +12,63 @@ function formatHMS(ms) {
|
||||||
async function va_projects_log_list_function(req, res) {
|
async function va_projects_log_list_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const { projectId, from, to } = req.params;
|
const { projectId, from, to } = req.params;
|
||||||
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
if (!projectId) {
|
if (!accessToken) {
|
||||||
return res.status(400).json({ success: false, message: "Missing projectId" });
|
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 now = new Date();
|
||||||
const fromDate = from && from !== "undefined" ? new Date(from) : new Date(now.setHours(0, 0, 0, 0));
|
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();
|
const toDate = to && to !== "undefined" ? new Date(to) : new Date();
|
||||||
toDate.setHours(23, 59, 59, 999);
|
toDate.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
// 🧩 Fetch Project
|
let projectList = [];
|
||||||
const project = await ProjectModel.findById(projectId).populate("vaAssigned", {
|
if (projectId) {
|
||||||
"userDetails.firstName": 1,
|
const project = await ProjectModel.findById(projectId);
|
||||||
"userDetails.lastName": 1,
|
|
||||||
"userDetails.email": 1,
|
|
||||||
status: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return res.status(404).json({ success: false, message: "Project not found" });
|
return res.status(404).json({ success: false, message: "Project not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaList = project.vaAssigned || [];
|
// Check if VA is assigned to this project
|
||||||
if (!vaList.length) {
|
const isAssigned = project.vaAssigned.some(id => id.toString() === vaId.toString());
|
||||||
return res.status(200).json({ success: true, message: "No VAs assigned to this project", data: [] });
|
if (!isAssigned) {
|
||||||
|
return res.status(403).json({ success: false, message: "VA not assigned to this project" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalProjectWorkMs = 0;
|
projectList = [project];
|
||||||
let activeCount = 0;
|
} else {
|
||||||
|
projectList = await ProjectModel.find({ vaAssigned: vaId });
|
||||||
|
}
|
||||||
|
|
||||||
const vaReport = [];
|
if (!projectList.length) {
|
||||||
|
return res.status(200).json({ success: true, message: "No assigned projects found", data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
// 🔁 Loop through each VA assigned to the project
|
const finalReport = [];
|
||||||
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({
|
const logs = await LoggedModel.find({
|
||||||
vaId,
|
vaId,
|
||||||
projectId,
|
projectId: project._id,
|
||||||
time: { $gte: fromDate, $lte: toDate }
|
time: { $gte: fromDate, $lte: toDate }
|
||||||
}).sort({ time: 1 }).lean();
|
})
|
||||||
|
.sort({ time: 1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
let totalWorkMs = 0;
|
let totalWorkMs = 0;
|
||||||
let currentLogin = null;
|
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) {
|
if (currentLogin) {
|
||||||
totalWorkMs += new Date() - new Date(currentLogin.time);
|
totalWorkMs += new Date() - new Date(currentLogin.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalProjectWorkMs += totalWorkMs;
|
|
||||||
|
|
||||||
// 🟢 Determine Active / Away status
|
|
||||||
const lastLog = logs[logs.length - 1];
|
const lastLog = logs[logs.length - 1];
|
||||||
let activityStatus = "AWAY";
|
const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY";
|
||||||
if (lastLog?.loggedType === "LOGIN") {
|
|
||||||
activityStatus = "ACTIVE";
|
|
||||||
activeCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
vaReport.push({
|
finalReport.push({
|
||||||
vaId,
|
projectId: project._id,
|
||||||
name: `${va.userDetails?.firstName || ""} ${va.userDetails?.lastName || ""}`.trim(),
|
projectName: project.name,
|
||||||
email: vaEmail,
|
|
||||||
status: va.status,
|
|
||||||
activityStatus,
|
activityStatus,
|
||||||
totalWorkTime: formatHMS(totalWorkMs),
|
totalWorkTime: formatHMS(totalWorkMs),
|
||||||
totalWorkMs,
|
totalWorkMs,
|
||||||
|
totalLogs: logs.length
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📊 Prepare Summary
|
// Summary for current VA
|
||||||
const totalVAs = vaReport.length;
|
const totalProjects = finalReport.length;
|
||||||
const awayCount = totalVAs - activeCount;
|
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({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
projectId,
|
vaId,
|
||||||
projectName: project.name,
|
vaName: `${vaUser.userDetails.firstName} ${vaUser.userDetails.lastName}`,
|
||||||
totalVAs,
|
totalProjects,
|
||||||
activeVAs: activeCount,
|
activeProjects,
|
||||||
awayVAs: awayCount,
|
awayProjects,
|
||||||
totalProjectWorkTime: formatHMS(totalProjectWorkMs),
|
totalWorkTime: formatHMS(totalMs),
|
||||||
totalProjectWorkMs,
|
totalWorkMs: totalMs,
|
||||||
data: vaReport
|
data: finalReport
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,109 @@
|
||||||
import { VAModel, LoggedModel } from "../../models/vaModel.js";
|
import { VAModel, ProjectModel, LoggedModel } from "../../models/vaModel.js";
|
||||||
|
|
||||||
// Getting VA details by username
|
// Getting VA details by username
|
||||||
async function va_username_callback(req, res) {
|
async function va_username_callback(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userName } = req.params;
|
const { userName } = req.params;
|
||||||
if (!userName) {
|
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)
|
// Find VA by firstName or lastName or email (depending on how you store username)
|
||||||
const va = await VAModel.findOne({
|
const vaUser = await VAModel.findOne({
|
||||||
$or: [
|
$or: [
|
||||||
{ _id: userName },
|
|
||||||
{ "userDetails.email": userName },
|
{ "userDetails.email": userName },
|
||||||
{ "userDetails.userName": userName },
|
|
||||||
{ "userDetails.firstName": userName },
|
{ "userDetails.firstName": userName },
|
||||||
{ "userDetails.lastName": 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" });
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Latest log entry to determine active state
|
const vaId = vaUser._id;
|
||||||
const lastLog = await LoggedModel.findOne({ vaId: va._id }).sort({ time: -1 }).lean().exec();
|
|
||||||
const active = lastLog ? String(lastLog.loggedType).toUpperCase() === "LOGIN" : false;
|
|
||||||
|
|
||||||
// Fetch all logs for this VA (ordered ascending) to compute logged hours
|
// Get all logs for this VA
|
||||||
const logs = await LoggedModel.find({ vaId: va._id }).sort({ time: 1 }).lean().exec();
|
const logs = await LoggedModel.find({ vaId }).sort({ time: 1 }).populate("projectId", "name").lean();
|
||||||
|
|
||||||
let totalSeconds = 0;
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")}`;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
|
if (!logs.length) {
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
message: "No logs found for this VA",
|
||||||
data: {
|
data: {
|
||||||
id: va._id,
|
va: vaUser,
|
||||||
name: name || null,
|
totalWorkTime: "00h 00m 00s",
|
||||||
email,
|
totalWorkMs: 0,
|
||||||
active,
|
projects: [],
|
||||||
totalHours,
|
},
|
||||||
lastLog: lastLog || null,
|
});
|
||||||
projects
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute time spent on each project
|
||||||
|
let totalWorkMs = 0;
|
||||||
|
const projectWork = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
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) {
|
} 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" });
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,19 +25,31 @@ const userDetailsSchema = new Schema({
|
||||||
address: {
|
address: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
city: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
bankName: {
|
paymentMethod: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
accountHolderName: {
|
payoneerAccount: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
accountNumber: {
|
veemAccount: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
routingNumber: {
|
phone: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
skype: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -61,6 +73,9 @@ const projectSchema = new Schema({
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
hourlyRate: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
totalLoggedHours: {
|
totalLoggedHours: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
|
@ -77,16 +92,93 @@ const projectSchema = new Schema({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------------------- Client Schema ---------------------- */
|
/* ---------------------- Client Details Schema ---------------------- */
|
||||||
const clientSchema = new Schema({
|
const clientDetailsSchema = new Schema({
|
||||||
userDetails: {
|
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
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: clientDetailsSchema,
|
||||||
vaIds: [
|
vaIds: [
|
||||||
{
|
{
|
||||||
type: Types.ObjectId,
|
type: Types.ObjectId,
|
||||||
|
|
@ -99,9 +191,14 @@ const clientSchema = new Schema({
|
||||||
ref: "Project",
|
ref: "Project",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
totalLoggedHours: {
|
accessToken: {
|
||||||
type: Number,
|
type: String,
|
||||||
|
default: null
|
||||||
},
|
},
|
||||||
|
refreshToken: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import authRoutes from './auth.routes.js';
|
import authRoutes from './auth.routes.js';
|
||||||
import vaRoutes from './va.routes.js';
|
import vaRoutes from './va.routes.js';
|
||||||
|
import clientRoutes from './client.routes.js';
|
||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ v1Routes.get("/auth", function(req, res) {
|
||||||
v1Routes.use("/auth_callback", authRoutes);
|
v1Routes.use("/auth_callback", authRoutes);
|
||||||
|
|
||||||
v1Routes.use("/va", vaRoutes);
|
v1Routes.use("/va", vaRoutes);
|
||||||
|
v1Routes.use("/client", clientRoutes);
|
||||||
|
|
||||||
v1Routes.get("/", function(req, res) {
|
v1Routes.get("/", function(req, res) {
|
||||||
res.json({"connected": true})
|
res.json({"connected": true})
|
||||||
|
|
|
||||||
|
|
@ -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_filter_search } from "../../controllers/va/vaFilterSearch.controller.js";
|
||||||
import { va_log_callback } from "../../controllers/va/vaLog/vaLog.controller.js";
|
import { va_log_callback } from "../../controllers/va/vaLog/vaLog.controller.js";
|
||||||
import { va_callback } from "../../controllers/va/vaLog/vaListLog.controller.js";
|
import { va_callback } from "../../controllers/va/vaLog/vaListLog.controller.js";
|
||||||
import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
|
|
||||||
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
|
|
||||||
import { va_project_log_callback } from "../../controllers/va/vaProjects/vaProjectLog.controller.js";
|
import { va_project_log_callback } from "../../controllers/va/vaProjects/vaProjectLog.controller.js";
|
||||||
import { va_projects_callback } from "../../controllers/va/vaProjects/vaProjects.controller.js";
|
|
||||||
import { authValidator } from "../../middlewares/auth.middleware.js";
|
import { authValidator } from "../../middlewares/auth.middleware.js";
|
||||||
import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrackWork.controller.js";
|
import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrackWork.controller.js";
|
||||||
import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js";
|
import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js";
|
||||||
|
|
@ -31,22 +28,23 @@ vaRoutes.post("/register", va_register); // Register a new VA
|
||||||
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
|
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
|
||||||
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
|
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
|
||||||
|
|
||||||
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
|
||||||
// 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("/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
|
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
|
||||||
vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
|
vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
|
||||||
|
|
||||||
|
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
|
||||||
|
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
|
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
|
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`
|
// VA filter search: supports query params `name`, `email`, `from`, `to`
|
||||||
|
|
||||||
|
|
||||||
|
// // API endpoint (for frontend to call)
|
||||||
|
// vaRoutes.post("/forgot-password", va_request_password_reset);
|
||||||
|
|
||||||
|
// // EJS rendering routes
|
||||||
|
// vaRoutes.get("/reset-form", va_render_reset_form);
|
||||||
|
// vaRoutes.post("/reset", va_process_reset);
|
||||||
|
|
||||||
|
|
||||||
export default vaRoutes;
|
export default vaRoutes;
|
||||||
|
|
@ -47,6 +47,10 @@ app.get('/varegister', (req, res) => {
|
||||||
res.render('va_register', { title: "VA Registration" });
|
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) => {
|
app.get('/health', (req, res) => {
|
||||||
res.status(201).send({ status: "Server is healthy" });
|
res.status(201).send({ status: "Server is healthy" });
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Client Registration</title>
|
||||||
|
|
||||||
|
<link href="/public/css/va-register.css" rel="stylesheet">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Sidebar for desktop -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-placeholder">
|
||||||
|
<h2>DocTracker</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="progress-steps">
|
||||||
|
<div class="step active" data-step="1" onclick="goToStep(1)">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
<!-- <div class="step-number">✓</div> -->
|
||||||
|
<div class="step-text">Create Your Account</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="2" onclick="goToStep(2)">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<div class="step-text">Business Information</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="3" onclick="goToStep(3)">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-text">Billing Address</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="4" onclick="goToStep(4)">
|
||||||
|
<div class="step-number">4</div>
|
||||||
|
<div class="step-text">Authorized Signatory</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="5" onclick="goToStep(5)">
|
||||||
|
<div class="step-number">5</div>
|
||||||
|
<div class="step-text">Payment Details</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="6" onclick="goToStep(6)">
|
||||||
|
<div class="step-number">6</div>
|
||||||
|
<div class="step-text">Done</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Mobile progress indicator -->
|
||||||
|
<div class="mobile-progress">
|
||||||
|
<ol>
|
||||||
|
<li class="step active" data-step="1" onclick="goToStep(1)">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
<!-- <div class="step-number">✓</div> -->
|
||||||
|
<div class="step-text">Create Account</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="2" onclick="goToStep(2)">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<div class="step-text">Business Info</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="3" onclick="goToStep(3)">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-text">Billing</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="4" onclick="goToStep(4)">
|
||||||
|
<div class="step-number">4</div>
|
||||||
|
<div class="step-text">Signatory</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="5" onclick="goToStep(5)">
|
||||||
|
<div class="step-number">5</div>
|
||||||
|
<div class="step-text">Payment</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="6" onclick="goToStep(6)">
|
||||||
|
<div class="step-number">6</div>
|
||||||
|
<div class="step-text">Done</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form container -->
|
||||||
|
<div class="form-container">
|
||||||
|
<h2 class="form-title-sec">Client Registration</h2>
|
||||||
|
<div class="form-card">
|
||||||
|
<!-- Step 1: Create Account -->
|
||||||
|
<div class="form-step active" id="step-1">
|
||||||
|
<h2 class="form-title">Create Your Account</h2>
|
||||||
|
<form id="account-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="company-name" class="input-field" placeholder=" " required>
|
||||||
|
<label for="company-name" class="input-label">Company / Client Name *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="email" id="email" class="input-field" placeholder=" " required>
|
||||||
|
<label for="email" class="input-label">Email *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<div></div> <!-- Empty div for spacing -->
|
||||||
|
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Business Information -->
|
||||||
|
<div class="form-step" id="step-2">
|
||||||
|
<h2 class="form-title">Business Information</h2>
|
||||||
|
<form id="business-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="address" class="input-field" placeholder=" " required>
|
||||||
|
<label for="address" class="input-label">Address *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="city" class="input-field" placeholder=" " required>
|
||||||
|
<label for="city" class="input-label">City *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<!-- <input type="text" id="state" class="input-field" placeholder=" " required> -->
|
||||||
|
<select id="state" class="select-field" required>
|
||||||
|
<option value=""></option>
|
||||||
|
<!-- States will be populated by JavaScript -->
|
||||||
|
</select>
|
||||||
|
<label for="state" class="input-label">State *</label>
|
||||||
|
<div class="location-loading" id="state-loading">Detecting your location...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="zip" class="input-field" placeholder=" " required>
|
||||||
|
<label for="zip" class="input-label">ZIP *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="nextStep(3)">Continue</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Billing Address -->
|
||||||
|
<div class="form-step" id="step-3">
|
||||||
|
<h2 class="form-title">Billing Address</h2>
|
||||||
|
<form id="billing-form">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" id="use-business-address">
|
||||||
|
<label for="use-business-address">Use business details for billing address</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="billing-name" class="input-field" placeholder=" " required>
|
||||||
|
<label for="billing-name" class="input-label">Name *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="billing-address" class="input-field" placeholder=" " required>
|
||||||
|
<label for="billing-address" class="input-label">Address *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="billing-city" class="input-field" placeholder=" " required>
|
||||||
|
<label for="billing-city" class="input-label">City *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<!-- <input type="text" id="billing-state" class="input-field" placeholder=" " required> -->
|
||||||
|
<select id="billing-state" class="select-field" required>
|
||||||
|
<option value=""></option>
|
||||||
|
<!-- States will be populated by JavaScript -->
|
||||||
|
</select>
|
||||||
|
<label for="billing-state" class="input-label">State *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="billing-zip" class="input-field" placeholder=" " required>
|
||||||
|
<label for="billing-zip" class="input-label">ZIP *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" id="use-account-email">
|
||||||
|
<label for="use-account-email">Use account email as primary billing contact</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="email" id="billing-email" class="input-field" placeholder=" " required>
|
||||||
|
<label for="billing-email" class="input-label">Email Address *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="nextStep(4)">Continue</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Authorized Signatory -->
|
||||||
|
<div class="form-step" id="step-4">
|
||||||
|
<h2 class="form-title">Authorized Signatory</h2>
|
||||||
|
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
|
||||||
|
We will use the details below in the agreement that we will send to your prior to entering the project.
|
||||||
|
</p>
|
||||||
|
<form id="signatory-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="position" class="input-field" placeholder=" " required>
|
||||||
|
<label for="position" class="input-label">Position (Owner / CEO / Manager / etc.) *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="signatory-name" class="input-field" placeholder=" " required>
|
||||||
|
<label for="signatory-name" class="input-label">Name *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="email" id="signatory-email" class="input-field" placeholder=" " required>
|
||||||
|
<label for="signatory-email" class="input-label">Email *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="nextStep(5)">Continue</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 5: Payment Details -->
|
||||||
|
<div class="form-step" id="step-5">
|
||||||
|
<h2 class="form-title">Payment Details</h2>
|
||||||
|
<form id="payment-form">
|
||||||
|
<div class="payment-option" onclick="selectPaymentOption('card')">
|
||||||
|
<h4>Credit or Debit Card</h4>
|
||||||
|
<p>Pay securely with your card</p>
|
||||||
|
</div>
|
||||||
|
<div id="card-details" class="payment-details">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="card-number" class="input-field" placeholder=" " required>
|
||||||
|
<label for="card-number" class="input-label">Card Number *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="expiry-date" class="input-field" placeholder=" " required>
|
||||||
|
<label for="expiry-date" class="input-label">Expiry Date (MM/YY) *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="security-code" class="input-field" placeholder=" " required>
|
||||||
|
<label for="security-code" class="input-label">Security Code *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="card-zip" class="input-field" placeholder=" " required>
|
||||||
|
<label for="card-zip" class="input-label">ZIP Code *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="payment-option" onclick="selectPaymentOption('bank')">
|
||||||
|
<h4>Bank Account (ACR)</h4>
|
||||||
|
<p>Direct bank transfer</p>
|
||||||
|
</div>
|
||||||
|
<div id="bank-details" class="payment-details">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="bank-name" class="input-field" placeholder=" " required>
|
||||||
|
<label for="bank-name" class="input-label">Bank Name *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="account-number" class="input-field" placeholder=" " required>
|
||||||
|
<label for="account-number" class="input-label">Account Number *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="routing-number" class="input-field" placeholder=" " required>
|
||||||
|
<label for="routing-number" class="input-label">Routing Number *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<select id="account-type" class="select-field" required>
|
||||||
|
<option value=""></option>
|
||||||
|
<option value="checking">Checking</option>
|
||||||
|
<option value="savings">Savings</option>
|
||||||
|
</select>
|
||||||
|
<label for="account-type" class="input-label">Account Type *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="account-holder-name" class="input-field" placeholder=" " required>
|
||||||
|
<label for="account-holder-name" class="input-label">Account Holder Name *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="prevStep(4)">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitForm()">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 6: Success -->
|
||||||
|
<div class="form-step" id="step-6">
|
||||||
|
<div class="success-message">
|
||||||
|
<div class="success-icon">✓</div>
|
||||||
|
<h3>Registration Complete!</h3>
|
||||||
|
<p>Your account has been successfully created. You will receive a confirmation email shortly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/public/js/va-client-register.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -3,13 +3,12 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>VA Registration</title>
|
<title>Virtual Assistant Registration</title>
|
||||||
|
|
||||||
<!-- Bootstrap -->
|
<link href="/public/css/va-register.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
/* body {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
font-family: 'Segoe UI', sans-serif;
|
font-family: 'Segoe UI', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
@ -89,117 +88,280 @@
|
||||||
.card-registration img {
|
.card-registration img {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<section class="h-100 py-5">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center align-items-center">
|
<!-- Sidebar for desktop -->
|
||||||
<div class="col-lg-10 col-xl-9">
|
<div class="sidebar">
|
||||||
<div class="card card-registration shadow-lg">
|
<div class="logo">
|
||||||
<div class="row g-0">
|
<div class="logo-placeholder">
|
||||||
|
<h2>DocTracker</h2>
|
||||||
<!-- Left Image -->
|
</div>
|
||||||
<div class="col-xl-5 d-none d-xl-block">
|
</div>
|
||||||
<img
|
<nav class="progress-steps">
|
||||||
src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-registration/img4.webp"
|
<div class="step active" data-step="1" onclick="goToStep(1)">
|
||||||
alt="Registration"
|
<div class="step-number">1</div>
|
||||||
class="img-fluid"
|
<div class="step-text">Account Setup</div>
|
||||||
/>
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="2" onclick="goToStep(2)">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<div class="step-text">Personal Information</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="3" onclick="goToStep(3)">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-text">Contact Information</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="4" onclick="goToStep(4)">
|
||||||
|
<div class="step-number">4</div>
|
||||||
|
<div class="step-text">Payment Details</div>
|
||||||
|
<div class="step-connector"></div>
|
||||||
|
</div>
|
||||||
|
<div class="step" data-step="5" onclick="goToStep(5)">
|
||||||
|
<div class="step-number">5</div>
|
||||||
|
<div class="step-text">Done</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Form -->
|
<!-- Main content -->
|
||||||
<div class="col-xl-7 bg-white">
|
<div class="main-content">
|
||||||
<div class="card-body text-black">
|
<!-- Mobile progress indicator -->
|
||||||
<h3 class="text-center mb-4">Virtual Assistant Registration</h3>
|
<div class="mobile-progress">
|
||||||
|
<ol>
|
||||||
<form id="vaForm">
|
<li class="step active" data-step="1" onclick="goToStep(1)">
|
||||||
<div class="row">
|
<div class="step-number">1</div>
|
||||||
<div class="col-md-6 mb-3">
|
<div class="step-text">Account</div>
|
||||||
<label class="form-label" for="firstName">First Name *</label>
|
</li>
|
||||||
<input type="text" id="firstName" name="userDetails[firstName]" class="form-control form-control-lg" required>
|
<li class="step" data-step="2" onclick="goToStep(2)">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<div class="step-text">Personal</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="3" onclick="goToStep(3)">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-text">Contact</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="4" onclick="goToStep(4)">
|
||||||
|
<div class="step-number">4</div>
|
||||||
|
<div class="step-text">Payment</div>
|
||||||
|
</li>
|
||||||
|
<li class="step" data-step="5" onclick="goToStep(5)">
|
||||||
|
<div class="step-number">5</div>
|
||||||
|
<div class="step-text">Done</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label" for="lastName">Last Name *</label>
|
<!-- Form container -->
|
||||||
<input type="text" id="lastName" name="userDetails[lastName]" class="form-control form-control-lg" required>
|
<div class="form-container">
|
||||||
|
<h2 class="form-title-sec">Virtual Assistant Registration</h2>
|
||||||
|
<div class="form-card">
|
||||||
|
<!-- Step 1: Account Setup -->
|
||||||
|
<div class="form-step active" id="step-1">
|
||||||
|
<h2 class="form-title">Account Setup</h2>
|
||||||
|
<form id="account-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="email" id="email" class="input-field" placeholder=" " required>
|
||||||
|
<label for="email" class="input-label">Email Address *</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label class="form-label" for="email">Email *</label>
|
<div class="input-container">
|
||||||
<input type="email" id="email" name="userDetails[email]" class="form-control form-control-lg" required>
|
<input type="password" id="password" class="input-field" placeholder=" " required>
|
||||||
|
<label for="password" class="input-label">Create Password *</label>
|
||||||
|
</div>
|
||||||
|
<div class="password-strength">
|
||||||
|
<div class="password-strength-bar" id="password-strength-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="password-actions">
|
||||||
|
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
|
||||||
|
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Use at least 8 characters with a mix of letters, numbers, and symbols</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="form-group">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="input-container">
|
||||||
<label class="form-label" for="age">Age</label>
|
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required>
|
||||||
<input type="text" id="age" name="userDetails[age]" class="form-control form-control-lg">
|
<label for="confirmPassword" class="input-label">Confirm Password *</label>
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label" for="dob">Date of Birth</label>
|
|
||||||
<input type="date" id="dob" name="userDetails[dob]" class="form-control form-control-lg">
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="form-actions">
|
||||||
<div class="col-md-6 mb-3">
|
<div></div>
|
||||||
<label class="form-label" for="password">Password *</label>
|
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button>
|
||||||
<input type="password" id="password" name="userDetails[password]" class="form-control form-control-lg" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label" for="cPassword">Confirm Password *</label>
|
|
||||||
<input type="password" id="cPassword" name="userDetails[cPassword]" class="form-control form-control-lg" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="address">Address</label>
|
|
||||||
<input type="text" id="address" name="userDetails[address]" class="form-control form-control-lg">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5>Bank Details</h5>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="bankName">Bank Name</label>
|
|
||||||
<input type="text" id="bankName" name="userDetails[bankName]" class="form-control form-control-lg" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="accountHolderName">Account Holder Name</label>
|
|
||||||
<input type="text" id="accountHolderName" name="userDetails[accountHolderName]" class="form-control form-control-lg" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="accountNumber">Account Number</label>
|
|
||||||
<input type="text" id="accountNumber" name="userDetails[accountNumber]" class="form-control form-control-lg" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="routingNumber">Routing Number</label>
|
|
||||||
<input type="text" id="routingNumber" name="userDetails[routingNumber]" class="form-control form-control-lg" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="form-label" for="status">Status</label>
|
|
||||||
<select id="status" name="status" class="form-select form-select-lg">
|
|
||||||
<option value="FULLTIME">Full-Time</option>
|
|
||||||
<option value="PARTTIME">Part-Time</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-end gap-2 pt-3">
|
|
||||||
<button type="reset" class="btn btn-light btn-lg">Reset</button>
|
|
||||||
<button type="submit" class="btn btn-warning btn-lg">Submit</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Personal Information -->
|
||||||
|
<div class="form-step" id="step-2">
|
||||||
|
<h2 class="form-title">Personal Information</h2>
|
||||||
|
|
||||||
|
<div class="profile-incomplete" id="profile-incomplete" style="display: none;">
|
||||||
|
Profile is not yet complete, first name is missing
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form id="personal-form">
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="firstName" class="input-field" placeholder=" " required>
|
||||||
|
<label for="firstName" class="input-label">First Name *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="lastName" class="input-field" placeholder=" " required>
|
||||||
|
<label for="lastName" class="input-label">Last Name *</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="number" id="age" class="input-field" placeholder=" " min="18" max="100">
|
||||||
|
<label for="age" class="input-label">Age</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="date" id="dob" class="input-field" placeholder=" ">
|
||||||
|
<label for="dob" class="input-label">Date of Birth</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="address" class="input-field" placeholder=" " required>
|
||||||
|
<label for="address" class="input-label">Address *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="city" class="input-field" placeholder=" " required>
|
||||||
|
<label for="city" class="input-label">City / Town *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<select id="country" class="select-field" required>
|
||||||
|
<option value=""></option>
|
||||||
|
</select>
|
||||||
|
<label for="country" class="input-label">Country *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<select id="state" class="select-field" required>
|
||||||
|
<option value=""></option>
|
||||||
|
</select>
|
||||||
|
<label for="state" class="input-label">State / Region *</label>
|
||||||
|
<div class="location-loading" id="state-loading">Loading states...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="nextStep(3)">Continue</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Contact Information -->
|
||||||
|
<div class="form-step" id="step-3">
|
||||||
|
<h2 class="form-title">Contact Information</h2>
|
||||||
|
<form id="contact-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone" class="input-label">Phone Number *</label>
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="tel" id="phone" class="input-field" placeholder=" " required>
|
||||||
|
<label for="phone" class="input-label" style="left: 40px;">Phone Number</label>
|
||||||
|
<span style="position: absolute; left: 12px; top: 50%; transform: translateY(-50%);">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="skype" class="input-field" placeholder=" ">
|
||||||
|
<label for="skype" class="input-label">Skype ID</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="nextStep(4)">Continue</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Payment Details -->
|
||||||
|
<div class="form-step" id="step-4">
|
||||||
|
<h2 class="form-title">Payment Details</h2>
|
||||||
|
<form id="payment-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="input-label">Payment Method *</label>
|
||||||
|
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
|
||||||
|
<h4>Payoneer</h4>
|
||||||
|
<p>Receive payments via Payoneer</p>
|
||||||
|
</div>
|
||||||
|
<div id="payoneer-details" class="payment-details">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " required>
|
||||||
|
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="payment-option" onclick="selectPaymentMethod('veem')">
|
||||||
|
<h4>Veem</h4>
|
||||||
|
<p>Receive payments via Veem</p>
|
||||||
|
</div>
|
||||||
|
<div id="veem-details" class="payment-details">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="veemAccount" class="input-field" placeholder=" " required>
|
||||||
|
<label for="veemAccount" class="input-label">Veem Account Number *</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitForm()">Complete Registration</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 5: Success -->
|
||||||
|
<div class="form-step" id="step-5">
|
||||||
|
<div class="success-message">
|
||||||
|
<div class="success-icon">✓</div>
|
||||||
|
<h3>Registration Complete!</h3>
|
||||||
|
<p>Your Virtual Assistant account has been successfully created..</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script src="/public/js/va-register.js"></script>
|
<script src="/public/js/va-register.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue