This commit is contained in:
=bhagwanSingh 2025-11-12 15:39:16 +05:30
commit 47f8d96a02
21 changed files with 3454 additions and 361 deletions

155
package-lock.json generated
View File

@ -45,8 +45,11 @@
"@babel/cli": "^7.26.4",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^9.2.1",
"copy-webpack-plugin": "^12.0.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"terser-webpack-plugin": "^5.3.11",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
@ -1828,9 +1831,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@ -2546,6 +2549,44 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.24.4",
"caniuse-lite": "^1.0.30001702",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -3015,9 +3056,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001692",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
"integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==",
"version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
"dev": true,
"funding": [
{
@ -3973,9 +4014,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4726,6 +4767,20 @@
"node": ">= 0.6"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -6604,6 +6659,25 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"license": "ISC"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -6782,6 +6856,16 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nth-check": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
@ -7244,6 +7328,42 @@
"node": ">=8"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -8423,6 +8543,16 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -8589,6 +8719,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",

View File

@ -1,70 +1,475 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
body {
background-color: #212529;
background-color: #fff;
color: #333;
}
.card-registration {
border-radius: 15px;
}
.card-registration .form-control {
border-radius: 0.5rem;
}
.form-label {
font-weight: 500;
}
.btn-warning {
background-color: #ffb300;
border: none;
color: #000;
font-weight: 600;
}
.btn-warning:hover {
background-color: #e6a000;
}
.form-outline .form-control+.form-label {
position: absolute;
top: 6px;
max-width: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
left: .75rem;
padding-top: .37rem;
pointer-events: none;
transform-origin: 0 0;
transition: all .2s ease-out;
color: var(--mdb-form-control-label-color);
margin-bottom: 0;
background-color: #ffffff;
}
.form-outline .form-control {
outline: none;
box-shadow: none;
}
.form-outline {
position: relative;
.container {
display: flex;
height: 100vh;
width: 100%;
}
.form-outline .form-control.not-required:placeholder-shown + .form-label,
.form-outline .form-control.not-required:empty + .form-label {
top: 6px;
/* Sidebar Styles */
.sidebar {
display: none;
flex-direction: column;
width: 344px;
background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0);
padding: 24px;
border-radius: 14px;
margin: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.form-outline .form-control:focus + .form-label,
.form-outline .form-control:valid + .form-label,
.form-outline .form-control.not-required:not(:placeholder-shown) + .form-label,
.form-outline .form-control.not-required:focus + .form-label {
top: -17px;
.logo {
display: flex;
justify-content: center;
margin-bottom: 32px;
}
.logo-placeholder img{
width: 100%;
height: auto;
max-width: 196px;
}
.logo-placeholder h2 {
font-size: 32px;
font-weight: 700;
color: #000;
text-align: center;
}
.card-registration .select-input.form-control[readonly]:not([disabled]) {
font-size: 1rem;
line-height: 2.15;
padding-left: .75em;
padding-right: .75em;
.form-title-sec {
font-size: 24px;
font-weight: 700;
color: #000;
text-align: center;
margin-bottom: 24px;
}
.progress-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.step {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 12px;
border: 2px solid transparent;
cursor: pointer;
position: relative;
}
.step.active {
border-color: #000000;
}
.step.completed .step-number {
background-color: #000000;
border-color: #000000;
color: white;
}
.step-number {
width: 34px;
height: 34px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #00000070;
font-weight: 600;
color: #000000b5;
transition: all 0.3s ease;
}
.step.active .step-number {
border-color: #000;
color: #000;
}
.step-text {
font-size: 14px;
color: #000000b5;
}
.step.active .step-text {
color: #000;
}
.step-connector {
height: 24px;
width: 2px;
background-color: #00000070;
margin-left: 16px;
position: absolute;
top: 46px;
left: 12px;
}
.step.active .step-connector {
background-color: #000;
}
/* Main Content Styles */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow-y: auto;
}
.mobile-progress {
background-color: #f4f4f5;
padding: 16px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow-x: auto;
}
.mobile-progress ol {
display: flex;
list-style: none;
gap: 12px;
min-width: max-content;
}
.mobile-progress .step {
flex-shrink: 0;
}
.form-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
}
.form-card {
width: 100%;
max-width: 450px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
padding: 24px;
}
.form-title {
text-align: center;
font-size: 20px;
font-weight: 600;
margin-bottom: 24px;
color: #000;
}
.form-step {
display: none;
}
.form-step.active {
display: block;
}
.form-group {
margin-bottom: 16px;
}
.input-container {
position: relative;
background-color: #f9fafb;
border-radius: 6px;
overflow: hidden;
padding: 2px 0;
}
.input-label {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: #6b7280;
transition: all 0.2s ease;
pointer-events: none;
}
.input-field, .select-field {
width: 100%;
padding: 18px 12px 14px;
border: none;
background: transparent;
outline: none;
font-size: 14px;
border-radius: 6px;
}
.select-field {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
border-radius: 6px;
}
.input-field:focus + .input-label,
.input-field:not(:placeholder-shown) + .input-label,
.select-field:focus + .input-label,
.select-field:valid + .input-label {
top: 2px;
font-size: 12px;
transform: none;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.checkbox-group input {
width: 16px;
height: 16px;
}
.checkbox-group label {
font-size: 14px;
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 24px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #000000;
color: white;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
background-color: #f3f4f6;
color: #374151;
}
.btn-secondary:hover {
background-color: #e5e7eb;
}
.btn-outline {
background-color: transparent;
border: 1px solid #070065;
color: #070065;
}
.btn-outline:hover {
background-color: #070065;
color: white;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.payment-option {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
cursor: pointer;
}
.payment-option.selected {
border-color: #000000;
background-color: #f0f9ff;
}
.payment-option h4 {
margin-bottom: 8px;
font-size: 16px;
}
.payment-option p {
font-size: 14px;
color: #6b7280;
}
.payment-details {
display: none;
margin: 4px 0 16px;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background-color: #bdbdbd;
}
.payment-details.active {
display: block;
}
.success-message {
text-align: center;
padding: 40px 20px;
}
.success-icon {
font-size: 48px;
color: #10b981;
margin-bottom: 16px;
}
.success-message h3 {
font-size: 24px;
margin-bottom: 8px;
}
.success-message p {
color: #6b7280;
margin-bottom: 24px;
}
.location-loading {
display: none;
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.row {
display: flex;
gap: 12px;
}
.row .form-group {
flex: 1;
}
.profile-incomplete {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
color: #856404;
}
.password-strength {
height: 5px;
background: #e9ecef;
border-radius: 5px;
margin-top: 5px;
overflow: hidden;
}
.password-strength-bar {
height: 100%;
width: 0;
border-radius: 5px;
transition: width 0.3s ease;
}
.password-weak {
background: #dc3545;
width: 25%;
}
.password-medium {
background: #ffc107;
width: 50%;
}
.password-strong {
background: #28a745;
width: 100%;
}
.password-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.password-toggle {
background: none;
border: none;
color: #070065;
cursor: pointer;
font-size: 12px;
}
.password-generate {
background: none;
border: none;
color: #070065;
cursor: pointer;
font-size: 12px;
text-decoration: underline;
}
.form-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.detect-location {
background: none;
border: none;
color: #070065;
cursor: pointer;
font-size: 12px;
text-decoration: underline;
margin-top: 5px;
}
/* Responsive styles */
@media (min-width: 1024px) {
.sidebar {
display: flex;
}
.mobile-progress {
display: none;
}
.form-title-sec {
font-size: 40px;
margin-bottom: 40px;
}
}

View File

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

View File

@ -1,46 +1,733 @@
// Example: Basic client-side validation or UX improvements
document.addEventListener("DOMContentLoaded", function () {
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const password = document.getElementById("password").value;
const cPassword = document.getElementById("cPassword").value;
// Form data object
const formData = {
userDetails: {}
};
if (password !== cPassword) {
alert("Passwords do not match!");
// Current step tracking
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;
}
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
const form = document.querySelector("form");
const formData = new FormData(form);
const payload = { userDetails: {} };
// Show target step
document.getElementById(`step-${step}`).classList.add('active');
document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => {
el.classList.add('active');
});
formData.forEach((value, key) => {
// handle nested names like userDetails[firstName]
if (key.startsWith("userDetails[")) {
const innerKey = key.match(/userDetails\[(.*?)\]/)[1];
payload.userDetails[innerKey] = value;
document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => {
el.classList.add('active');
});
currentStep = step;
}
// Function to get the maximum completed step
function getMaxCompletedStep() {
const completedSteps = document.querySelectorAll('.step.completed');
let maxStep = 0;
completedSteps.forEach(step => {
const stepNum = parseInt(step.getAttribute('data-step'));
if (stepNum > maxStep) maxStep = stepNum;
});
return maxStep;
}
// Function to navigate to next step
function nextStep(step) {
// Validate current step before proceeding
if (!validateStep(currentStep)) {
return;
}
// Save current step data
saveStepData(currentStep);
// Mark current step as completed
document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
el.classList.add('completed');
const stepNumber = el.querySelector('.step-number');
stepNumber.textContent = '✓';
stepNumber.style.backgroundColor = '#000000';
stepNumber.style.borderColor = '#000000';
stepNumber.style.color = 'white';
});
document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => {
el.classList.add('completed');
const stepNumber = el.querySelector('.step-number');
stepNumber.textContent = '✓';
stepNumber.style.backgroundColor = '#000000';
stepNumber.style.borderColor = '#000000';
stepNumber.style.color = 'white';
});
// Hide current step
document.getElementById(`step-${currentStep}`).classList.remove('active');
document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
el.classList.remove('active');
});
document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => {
el.classList.remove('active');
});
// Show next step
document.getElementById(`step-${step}`).classList.add('active');
document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => {
el.classList.add('active');
});
document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => {
el.classList.add('active');
});
currentStep = step;
}
// Function to navigate to previous step
function prevStep(step) {
// Hide current step
document.getElementById(`step-${currentStep}`).classList.remove('active');
document.querySelectorAll(`.step[data-step="${currentStep}"]`).forEach(el => {
el.classList.remove('active');
});
document.querySelectorAll(`.mobile-step[data-step="${currentStep}"]`).forEach(el => {
el.classList.remove('active');
});
// Show previous step
document.getElementById(`step-${step}`).classList.add('active');
document.querySelectorAll(`.step[data-step="${step}"]`).forEach(el => {
el.classList.add('active');
});
document.querySelectorAll(`.mobile-step[data-step="${step}"]`).forEach(el => {
el.classList.add('active');
});
currentStep = step;
}
// Function to validate current step
function validateStep(step) {
let isValid = true;
const form = document.getElementById(getFormId(step));
if (form) {
// For account setup step
if (step === 1) {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// Email validation
if (!validateEmail(email)) {
alert('Please enter a valid email address');
isValid = false;
}
// Password validation
if (password.length < 8) {
alert('Password must be at least 8 characters long');
isValid = false;
}
if (password !== confirmPassword) {
document.getElementById('password-match-error').style.display = 'block';
isValid = false;
} else {
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);
// }
// });
// });
// });

View File

@ -0,0 +1,65 @@
import jwt from "jsonwebtoken";
import { ClientModel } from "../../models/vaModel.js";
// Refresh Token VA
async function client_refresh_token_callback(req, res) {
try {
const { refreshToken } = req.body || {};
if (!refreshToken) {
return res.status(400).json({
success: false,
message: "Refresh token missing",
});
}
// 🔍 Verify refresh token
jwt.verify(
refreshToken,
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
async (err, decoded) => {
if (err) {
return res.status(403).json({
success: false,
message: "Invalid or expired refresh token",
});
}
// Check if user still exists
const user = await ClientModel.findById(decoded.id);
if (!user) {
return res.status(404).json({
success: false,
message: "User no longer exists",
});
}
// Generate a new access token
const newAccessToken = jwt.sign(
{
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerAccessKey",
{ expiresIn: "24h" }
);
await ClientModel.updateOne({ "userDetails.email": user.userDetails.email }, {
$set: { accessToken: newAccessToken }
})
return res.status(200).json({
success: true,
message: "New access token generated",
accessToken: newAccessToken,
});
}
);
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { client_refresh_token_callback };

View File

@ -0,0 +1,76 @@
import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.js";
import logger from "../../config/logger.js";
const vaClientService = new VaClientService(new VaClientRepository());
async function va_register_client(req, res) {
logger.info("ClientController - Register Client Hit ...");
try {
const payload = req.body || {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.name) {
return res.status(400).json({
success: false,
message: "Missing required fields: name and email are required"
});
}
const created = await vaClientService.registerClient(payload);
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
return res.status(201).json({
success: true,
data: created,
message: "Client registered successfully"
});
} catch (error) {
logger.error(`ClientController - Register error: ${error.message}`);
if (error.message === "User already exists with this email") {
return res.status(409).json({
success: false,
message: error.message
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
// Additional controller methods can be added here
async function get_client_by_email(req, res) {
try {
const { email } = req.params;
const client = await vaClientService.getClientByEmail(email);
if (!client) {
return res.status(404).json({
success: false,
message: "Client not found"
});
}
return res.status(200).json({
success: true,
data: client
});
} catch (error) {
logger.error(`ClientController - Get client error: ${error.message}`);
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
export {
va_register_client,
get_client_by_email
};

View File

@ -0,0 +1,79 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { ClientModel } from "../../models/vaModel.js";
// Login VA
async function va_login_client(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.password) {
return res.status(400).json({ success: false, message: "Please provide email and password" });
}
// Find user by email
const user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" });
}
// const saltRounds = 10;
// const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
// Compare password with stored hash
const isMatch = await bcrypt.compare(
userDetails.password,
user.userDetails.password
);
if (!isMatch) {
return res.status(401).json({ success: false, message: "Invalid password" });
}
// Generate JWT accessToken with 24h expiry
const accessToken = jwt.sign(
{
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey",
{ expiresIn: "24h" }
);
// Generate JWT refreshToken with 24h expiry
const refreshToken = jwt.sign(
{
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
{ expiresIn: "30d" }
);
await ClientModel.updateOne({ "userDetails.email": userDetails.email }, {
$set: { accessToken, refreshToken }
})
// Return accessToken and user info
return res.status(200).json({
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
},
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_login_client };

View File

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

View File

@ -1,3 +1,4 @@
import jwt from "jsonwebtoken";
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
function formatHMS(ms) {
@ -11,50 +12,63 @@ function formatHMS(ms) {
async function va_projects_log_list_function(req, res) {
try {
const { projectId, from, to } = req.params;
const accessToken = req.headers.token;
if (!projectId) {
return res.status(400).json({ success: false, message: "Missing projectId" });
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
// 📅 Date filtering
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
// Date filtering
const now = new Date();
const fromDate = from && from !== "undefined" ? new Date(from) : new Date(now.setHours(0, 0, 0, 0));
const toDate = to && to !== "undefined" ? new Date(to) : new Date();
toDate.setHours(23, 59, 59, 999);
// 🧩 Fetch Project
const project = await ProjectModel.findById(projectId).populate("vaAssigned", {
"userDetails.firstName": 1,
"userDetails.lastName": 1,
"userDetails.email": 1,
status: 1
});
let projectList = [];
if (projectId) {
const project = await ProjectModel.findById(projectId);
if (!project) {
return res.status(404).json({ success: false, message: "Project not found" });
}
const vaList = project.vaAssigned || [];
if (!vaList.length) {
return res.status(200).json({ success: true, message: "No VAs assigned to this project", data: [] });
// Check if VA is assigned to this project
const isAssigned = project.vaAssigned.some(id => id.toString() === vaId.toString());
if (!isAssigned) {
return res.status(403).json({ success: false, message: "VA not assigned to this project" });
}
let totalProjectWorkMs = 0;
let activeCount = 0;
projectList = [project];
} 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
for (const va of vaList) {
const vaId = va._id;
const vaEmail = va.userDetails?.email;
const finalReport = [];
// 🔎 Get all login/logout logs for this VA in the date range
for (const project of projectList) {
const logs = await LoggedModel.find({
vaId,
projectId,
projectId: project._id,
time: { $gte: fromDate, $lte: toDate }
}).sort({ time: 1 }).lean();
})
.sort({ time: 1 })
.lean();
let totalWorkMs = 0;
let currentLogin = null;
@ -68,46 +82,40 @@ async function va_projects_log_list_function(req, res) {
}
}
// if still logged in (no logout yet) — count up to now
// still logged in → count till now
if (currentLogin) {
totalWorkMs += new Date() - new Date(currentLogin.time);
}
totalProjectWorkMs += totalWorkMs;
// 🟢 Determine Active / Away status
const lastLog = logs[logs.length - 1];
let activityStatus = "AWAY";
if (lastLog?.loggedType === "LOGIN") {
activityStatus = "ACTIVE";
activeCount++;
}
const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY";
vaReport.push({
vaId,
name: `${va.userDetails?.firstName || ""} ${va.userDetails?.lastName || ""}`.trim(),
email: vaEmail,
status: va.status,
finalReport.push({
projectId: project._id,
projectName: project.name,
activityStatus,
totalWorkTime: formatHMS(totalWorkMs),
totalWorkMs,
totalLogs: logs.length
});
}
// 📊 Prepare Summary
const totalVAs = vaReport.length;
const awayCount = totalVAs - activeCount;
// Summary for current VA
const totalProjects = finalReport.length;
const activeProjects = finalReport.filter(r => r.activityStatus === "ACTIVE").length;
const awayProjects = totalProjects - activeProjects;
const totalMs = finalReport.reduce((acc, r) => acc + r.totalWorkMs, 0);
res.status(200).json({
success: true,
projectId,
projectName: project.name,
totalVAs,
activeVAs: activeCount,
awayVAs: awayCount,
totalProjectWorkTime: formatHMS(totalProjectWorkMs),
totalProjectWorkMs,
data: vaReport
vaId,
vaName: `${vaUser.userDetails.firstName} ${vaUser.userDetails.lastName}`,
totalProjects,
activeProjects,
awayProjects,
totalWorkTime: formatHMS(totalMs),
totalWorkMs: totalMs,
data: finalReport
});
} catch (err) {

View File

@ -1,89 +1,109 @@
import { VAModel, LoggedModel } from "../../models/vaModel.js";
import { VAModel, ProjectModel, LoggedModel } from "../../models/vaModel.js";
// Getting VA details by username
async function va_username_callback(req, res) {
try {
const { userName } = req.params;
if (!userName) {
return res.status(400).json({ success: false, message: "Missing userName param" });
return res.status(400).json({ success: false, message: "Missing username" });
}
// Find VA by common identifier fields (id, email, username, first/last name)
const va = await VAModel.findOne({
// Find VA by firstName or lastName or email (depending on how you store username)
const vaUser = await VAModel.findOne({
$or: [
{ _id: userName },
{ "userDetails.email": userName },
{ "userDetails.userName": userName },
{ "userDetails.firstName": userName },
{ "userDetails.lastName": userName },
{ "userDetails.fullName": userName }
]
}).lean().exec();
],
})
.populate("projectIds", "name totalLoggedHours")
.lean();
if (!va) {
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
// Latest log entry to determine active state
const lastLog = await LoggedModel.findOne({ vaId: va._id }).sort({ time: -1 }).lean().exec();
const active = lastLog ? String(lastLog.loggedType).toUpperCase() === "LOGIN" : false;
const vaId = vaUser._id;
// Fetch all logs for this VA (ordered ascending) to compute logged hours
const logs = await LoggedModel.find({ vaId: va._id }).sort({ time: 1 }).lean().exec();
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;
// Get all logs for this VA
const logs = await LoggedModel.find({ vaId }).sort({ time: 1 }).populate("projectId", "name").lean();
if (!logs.length) {
return res.status(200).json({
success: true,
message: "No logs found for this VA",
data: {
id: va._id,
name: name || null,
email,
active,
totalHours,
lastLog: lastLog || null,
projects
va: vaUser,
totalWorkTime: "00h 00m 00s",
totalWorkMs: 0,
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) {
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" });
}
}

View File

@ -25,19 +25,31 @@ const userDetailsSchema = new Schema({
address: {
type: String,
},
city: {
type: String,
},
state: {
type: String,
},
country: {
type: String,
},
password: {
type: String,
},
bankName: {
paymentMethod: {
type: String,
},
accountHolderName: {
payoneerAccount: {
type: String,
},
accountNumber: {
veemAccount: {
type: String,
},
routingNumber: {
phone: {
type: String,
},
skype: {
type: String,
},
});
@ -61,6 +73,9 @@ const projectSchema = new Schema({
type: String,
required: true,
},
hourlyRate: {
type: String,
},
totalLoggedHours: {
type: Number,
default: 0
@ -77,16 +92,93 @@ const projectSchema = new Schema({
],
});
/* ---------------------- Client Schema ---------------------- */
const clientSchema = new Schema({
userDetails: {
/* ---------------------- Client Details Schema ---------------------- */
const clientDetailsSchema = new Schema({
name: {
type: String,
},
email: {
type: String,
required: true,
unique: true,
},
address: {
type: String,
},
city: {
type: String,
},
state: {
type: String,
},
zip: {
type: String,
},
billingName: {
type: String,
},
billingEmail: {
type: String,
},
billingAddress: {
type: String,
},
billingCity: {
type: String,
},
billingState: {
type: String,
},
billingZip: {
type: String,
},
position: {
type: String,
},
signatoryName: {
type: String,
},
signatoryEmail: {
type: String,
},
paymentMethod: {
type: String,
},
bankName: {
type: String,
},
accountNumber: {
type: String,
},
routingNumber: {
type: String,
},
accountType: {
type: String,
},
accountHolderName: {
type: String,
},
cardNumber: {
type: String,
},
expiryDate: {
type: String,
},
securityCode: {
type: String,
},
cardZip: {
type: String,
},
password: {
type: String,
},
});
/* ---------------------- Client Schema ---------------------- */
const clientSchema = new Schema({
userDetails: clientDetailsSchema,
vaIds: [
{
type: Types.ObjectId,
@ -99,9 +191,14 @@ const clientSchema = new Schema({
ref: "Project",
},
],
totalLoggedHours: {
type: Number,
accessToken: {
type: String,
default: null
},
refreshToken: {
type: String,
default: null
}
});

View File

@ -0,0 +1,55 @@
import { ClientModel } from "../models/vaModel.js";
class VaClientRepository {
async create(data) {
try {
const newClient = new ClientModel({ ...data });
const response = await newClient.save();
return response;
} catch (error) {
throw error;
}
}
async findByEmail(email) {
try {
const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() });
return response;
} catch (error) {
throw error;
}
}
async findById(id) {
try {
const response = await ClientModel.findById(id);
return response;
} catch (error) {
throw error;
}
}
async update(id, data) {
try {
const response = await ClientModel.findByIdAndUpdate(
id,
{ $set: { ...data } },
{ new: true }
);
return response;
} catch (error) {
throw error;
}
}
async delete(id) {
try {
const response = await ClientModel.findByIdAndDelete(id);
return response;
} catch (error) {
throw error;
}
}
}
export default VaClientRepository;

View File

@ -0,0 +1,29 @@
import express from "express";
const clientRoutes = express.Router();
import { va_register_client } from "../../controllers/client/vaClient.controller.js";
import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.controller.js";
// clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA
clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.get("/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC)
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
// // API endpoint (for frontend to call)
// clientRoutes.post("/forgot-password", va_client_request_password_reset);
// // EJS rendering routes
// clientRoutes.get("/reset-form", va_client_render_reset_form);
// clientRoutes.post("/reset", va_client_process_reset);
export default clientRoutes;

View File

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

View File

@ -11,10 +11,7 @@ import { va_logout_callback } from "../../controllers/va/vaAuth/vaLogout.control
import { va_filter_search } from "../../controllers/va/vaFilterSearch.controller.js";
import { va_log_callback } from "../../controllers/va/vaLog/vaLog.controller.js";
import { va_callback } from "../../controllers/va/vaLog/vaListLog.controller.js";
import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
import { va_project_log_callback } from "../../controllers/va/vaProjects/vaProjectLog.controller.js";
import { va_projects_callback } from "../../controllers/va/vaProjects/vaProjects.controller.js";
import { authValidator } from "../../middlewares/auth.middleware.js";
import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrackWork.controller.js";
import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js";
@ -31,22 +28,23 @@ vaRoutes.post("/register", va_register); // Register a new VA
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC)
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)
vaRoutes.get("/search", va_filter_search);
vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID
vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project
@ -56,4 +54,13 @@ vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); /
vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report
// VA filter search: supports query params `name`, `email`, `from`, `to`
// // API endpoint (for frontend to call)
// vaRoutes.post("/forgot-password", va_request_password_reset);
// // EJS rendering routes
// vaRoutes.get("/reset-form", va_render_reset_form);
// vaRoutes.post("/reset", va_process_reset);
export default vaRoutes;

View File

@ -47,6 +47,10 @@ app.get('/varegister', (req, res) => {
res.render('va_register', { title: "VA Registration" });
});
app.get('/clientregister', (req, res) => {
res.render('va_client_register', { title: "Client Registration" });
});
app.get('/health', (req, res) => {
res.status(201).send({ status: "Server is healthy" });
})

View File

@ -0,0 +1,107 @@
import logger from "../config/logger.js";
class VaClientService {
constructor(vaClientRepository) {
this.clientRepository = vaClientRepository;
}
async registerClient(data) {
try {
// Check if user already exists
const existingUser = await this.clientRepository.findByEmail(data.userDetails.email);
if (existingUser) {
throw new Error("User already exists with this email");
}
// Sanitize and prepare data
const clientData = this.sanitizeClientData(data);
const response = await this.clientRepository.create(clientData);
return response;
} catch (error) {
logger.error("Client Service - registerClient error: " + error.message);
throw error;
}
}
async getClientByEmail(email) {
try {
const response = await this.clientRepository.findByEmail(email);
return response;
} catch (error) {
logger.error("Client Service - getClientByEmail error: " + error.message);
throw error;
}
}
async getClientById(id) {
try {
const response = await this.clientRepository.findById(id);
if (!response) throw new Error("Client not found");
return response;
} catch (error) {
logger.error("Client Service - getClientById error: " + error.message);
throw error;
}
}
async updateClient(id, data) {
try {
const response = await this.clientRepository.update(id, data);
if (!response) throw new Error("Client not found");
return response;
} catch (error) {
logger.error("Client Service - updateClient error: " + error.message);
throw error;
}
}
async deleteClient(id) {
try {
const response = await this.clientRepository.delete(id);
return response;
} catch (error) {
logger.error("Client Service - deleteClient error: " + error.message);
throw error;
}
}
// Helper method to sanitize data
sanitizeClientData(payload) {
const { userDetails } = payload;
return {
userDetails: {
name: userDetails.name,
email: userDetails.email.toLowerCase(),
address: userDetails.address || null,
city: userDetails.city || null,
state: userDetails.state || null,
zip: userDetails.zip || null,
billingName: userDetails.billingName || null,
billingAddress: userDetails.billingAddress || null,
billingCity: userDetails.billingCity || null,
billingState: userDetails.billingState || null,
billingZip: userDetails.billingZip || null,
billingEmail: userDetails.billingEmail || null,
position: userDetails.position || null,
signatoryName: userDetails.signatoryName || null,
signatoryEmail: userDetails.signatoryEmail || null,
paymentMethod: userDetails.paymentMethod || null,
bankName: userDetails.bankName || null,
accountNumber: userDetails.accountNumber || null,
routingNumber: userDetails.routingNumber || null,
accountType: userDetails.accountType || null,
accountHolderName: userDetails.accountHolderName || null,
cardNumber: userDetails.cardNumber || null,
expiryDate: userDetails.expiryDate || null,
securityCode: userDetails.securityCode || null,
cardZip: userDetails.cardZip || null,
},
vaIds: payload.vaIds || [],
projectIds: payload.projectIds || [],
};
}
}
export default VaClientService;

View File

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

View File

@ -3,13 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VA Registration</title>
<title>Virtual Assistant Registration</title>
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/public/css/va-register.css" rel="stylesheet">
<style>
body {
/* body {
background-color: #f8f9fa;
font-family: 'Segoe UI', sans-serif;
}
@ -89,117 +88,280 @@
.card-registration img {
display: none;
}
}
} */
</style>
</head>
<body>
<section class="h-100 py-5">
<div class="container">
<div class="row justify-content-center align-items-center">
<div class="col-lg-10 col-xl-9">
<div class="card card-registration shadow-lg">
<div class="row g-0">
<!-- Left Image -->
<div class="col-xl-5 d-none d-xl-block">
<img
src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-registration/img4.webp"
alt="Registration"
class="img-fluid"
/>
<!-- 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-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>
<!-- Right Form -->
<div class="col-xl-7 bg-white">
<div class="card-body text-black">
<h3 class="text-center mb-4">Virtual Assistant Registration</h3>
<form id="vaForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="firstName">First Name *</label>
<input type="text" id="firstName" name="userDetails[firstName]" class="form-control form-control-lg" required>
<!-- Main content -->
<div class="main-content">
<!-- Mobile progress indicator -->
<div class="mobile-progress">
<ol>
<li class="step active" data-step="1" onclick="goToStep(1)">
<div class="step-number">1</div>
<div class="step-text">Account</div>
</li>
<li class="step" data-step="2" onclick="goToStep(2)">
<div class="step-number">2</div>
<div class="step-text">Personal</div>
</li>
<li class="step" data-step="3" onclick="goToStep(3)">
<div class="step-number">3</div>
<div class="step-text">Contact</div>
</li>
<li class="step" data-step="4" onclick="goToStep(4)">
<div class="step-number">4</div>
<div class="step-text">Payment</div>
</li>
<li class="step" data-step="5" onclick="goToStep(5)">
<div class="step-number">5</div>
<div class="step-text">Done</div>
</li>
</ol>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="lastName">Last Name *</label>
<input type="text" id="lastName" name="userDetails[lastName]" class="form-control form-control-lg" required>
<!-- Form container -->
<div class="form-container">
<h2 class="form-title-sec">Virtual Assistant Registration</h2>
<div class="form-card">
<!-- Step 1: Account Setup -->
<div class="form-step active" id="step-1">
<h2 class="form-title">Account Setup</h2>
<form id="account-form">
<div class="form-group">
<div class="input-container">
<input type="email" id="email" class="input-field" placeholder=" " required>
<label for="email" class="input-label">Email Address *</label>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="email">Email *</label>
<input type="email" id="email" name="userDetails[email]" class="form-control form-control-lg" required>
<div class="form-group">
<div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required>
<label for="password" class="input-label">Create Password *</label>
</div>
<div class="password-strength">
<div class="password-strength-bar" id="password-strength-bar"></div>
</div>
<div class="password-actions">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
</div>
<div class="form-text">Use at least 8 characters with a mix of letters, numbers, and symbols</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="age">Age</label>
<input type="text" id="age" name="userDetails[age]" class="form-control form-control-lg">
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="dob">Date of Birth</label>
<input type="date" id="dob" name="userDetails[dob]" class="form-control form-control-lg">
<div class="form-group">
<div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required>
<label for="confirmPassword" class="input-label">Confirm Password *</label>
</div>
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="password">Password *</label>
<input type="password" id="password" name="userDetails[password]" class="form-control form-control-lg" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="cPassword">Confirm Password *</label>
<input type="password" id="cPassword" name="userDetails[cPassword]" class="form-control form-control-lg" required>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="address">Address</label>
<input type="text" id="address" name="userDetails[address]" class="form-control form-control-lg">
</div>
<h5>Bank Details</h5>
<div class="mb-3">
<label class="form-label" for="bankName">Bank Name</label>
<input type="text" id="bankName" name="userDetails[bankName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountHolderName">Account Holder Name</label>
<input type="text" id="accountHolderName" name="userDetails[accountHolderName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountNumber">Account Number</label>
<input type="text" id="accountNumber" name="userDetails[accountNumber]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="routingNumber">Routing Number</label>
<input type="text" id="routingNumber" name="userDetails[routingNumber]" class="form-control form-control-lg" required>
</div>
<div class="mb-4">
<label class="form-label" for="status">Status</label>
<select id="status" name="status" class="form-select form-select-lg">
<option value="FULLTIME">Full-Time</option>
<option value="PARTTIME">Part-Time</option>
</select>
</div>
<div class="d-flex justify-content-end gap-2 pt-3">
<button type="reset" class="btn btn-light btn-lg">Reset</button>
<button type="submit" class="btn btn-warning btn-lg">Submit</button>
<div class="form-actions">
<div></div>
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button>
</div>
</form>
</div>
<!-- Step 2: Personal Information -->
<div class="form-step" id="step-2">
<h2 class="form-title">Personal Information</h2>
<div class="profile-incomplete" id="profile-incomplete" style="display: none;">
Profile is not yet complete, first name is missing
</div>
<form id="personal-form">
<div class="row">
<div class="form-group">
<div class="input-container">
<input type="text" id="firstName" class="input-field" placeholder=" " required>
<label for="firstName" class="input-label">First Name *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="lastName" class="input-field" placeholder=" " required>
<label for="lastName" class="input-label">Last Name *</label>
</div>
</div>
</div>
</div>
</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>
</body>
</html>