Compare commits
No commits in common. "master" and "merge2" have entirely different histories.
|
|
@ -1,4 +1,3 @@
|
||||||
node_modules
|
node_modules
|
||||||
logs
|
logs
|
||||||
.env
|
.env
|
||||||
public/builds
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-ejs-layouts": "^2.5.1",
|
|
||||||
"express-minify": "^1.0.0",
|
"express-minify": "^1.0.0",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"html-minifier": "^4.0.0",
|
"html-minifier": "^4.0.0",
|
||||||
|
|
@ -41,8 +40,7 @@
|
||||||
"uglify-js": "^3.19.3",
|
"uglify-js": "^3.19.3",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
"wireguard-tools": "^0.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.26.4",
|
||||||
|
|
@ -4233,11 +4231,6 @@
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-ejs-layouts": {
|
|
||||||
"version": "2.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
|
|
||||||
"integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
|
|
||||||
},
|
|
||||||
"node_modules/express-minify": {
|
"node_modules/express-minify": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz",
|
||||||
|
|
@ -6205,12 +6198,6 @@
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.mergewith": {
|
|
||||||
"version": "4.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
|
||||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.once": {
|
"node_modules/lodash.once": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
|
@ -9351,15 +9338,6 @@
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wireguard-tools": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/wireguard-tools/-/wireguard-tools-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-oBnnSB6wXGEAB1ZVPfbfpX8WlHNw7wv45r13ILX73felVnrGpMxMjFcKGAsMeRfBOGzXcW0LgAWe+T53dRcI+g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash.mergewith": "^4.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wordwrap": {
|
"node_modules/wordwrap": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nodemon --inspect src/server.js",
|
"start": "nodemon --inspect src/server.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"deploy": "curl -X POST http://35.95.209.161:9000/hooks/deploy-va-middleware",
|
|
||||||
"build": "npx webpack --mode production",
|
"build": "npx webpack --mode production",
|
||||||
"dev": "npx webpack --mode development"
|
"dev": "npx webpack --mode development"
|
||||||
},
|
},
|
||||||
|
|
@ -25,7 +24,6 @@
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-ejs-layouts": "^2.5.1",
|
|
||||||
"express-minify": "^1.0.0",
|
"express-minify": "^1.0.0",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"html-minifier": "^4.0.0",
|
"html-minifier": "^4.0.0",
|
||||||
|
|
@ -46,8 +44,7 @@
|
||||||
"uglify-js": "^3.19.3",
|
"uglify-js": "^3.19.3",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
"wireguard-tools": "^0.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.26.4",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,52 +5,9 @@
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
|
||||||
--bg-primary: #ffffff;
|
|
||||||
--bg-secondary: #f8fafc;
|
|
||||||
--bg-card: #ffffff;
|
|
||||||
--bg-sidebar: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7);
|
|
||||||
--text-primary: #333333;
|
|
||||||
--text-secondary: #6b7280;
|
|
||||||
--text-muted: #9ca3af;
|
|
||||||
--border-color: #e5e7eb;
|
|
||||||
--border-dark: #374151;
|
|
||||||
--accent-color: #000000;
|
|
||||||
--accent-light: #070065;
|
|
||||||
--success-color: #10b981;
|
|
||||||
--error-color: #dc2626;
|
|
||||||
--warning-bg: #fff3cd;
|
|
||||||
--warning-border: #ffeaa7;
|
|
||||||
--warning-text: #856404;
|
|
||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--bg-primary: #121212;
|
|
||||||
--bg-secondary: #1e1e1e;
|
|
||||||
--bg-card: #1e1e1e;
|
|
||||||
--bg-sidebar: linear-gradient(to bottom, #2d2d2d, #1a3a3a, #2a264d);
|
|
||||||
--text-primary: #f5f5f5;
|
|
||||||
--text-secondary: #d1d5db;
|
|
||||||
--text-muted: #9ca3af;
|
|
||||||
--border-color: #374151;
|
|
||||||
--border-dark: #4b5563;
|
|
||||||
--accent-color: #ffffff;
|
|
||||||
--accent-light: #6366f1;
|
|
||||||
--success-color: #10b981;
|
|
||||||
--error-color: #ef4444;
|
|
||||||
--warning-bg: #422006;
|
|
||||||
--warning-border: #854d0e;
|
|
||||||
--warning-text: #fef3c7;
|
|
||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
||||||
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg-primary);
|
background-color: #fff;
|
||||||
color: var(--text-primary);
|
color: #333;
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|
@ -64,12 +21,11 @@ body {
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 388px;
|
width: 388px;
|
||||||
background: var(--bg-sidebar);
|
background: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
|
@ -81,24 +37,18 @@ body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-width: 196px;
|
max-width: 196px;
|
||||||
filter: brightness(var(--logo-brightness, 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .logo-placeholder img {
|
|
||||||
--logo-brightness: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-placeholder h2 {
|
.logo-placeholder h2 {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: #000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-title-sec {
|
.form-title-sec {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color:var(--text-primary);
|
color: #000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
@ -118,25 +68,15 @@ body {
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3s ease;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .step {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.active {
|
.step.active {
|
||||||
border-color: var(--accent-color);
|
border-color: #000000;
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .step.active {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.completed .step-number {
|
.step.completed .step-number {
|
||||||
background-color: var(--accent-color);
|
background-color: #000000;
|
||||||
border-color: var(--accent-color);
|
border-color: #000000;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,43 +87,30 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 2px solid var(--accent-color);
|
border: 2px solid #00000070;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-color);
|
color: #000000b5;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .step-number {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.active .step-number {
|
.step.active .step-number {
|
||||||
border-color:var(--text-primary);
|
border-color: #000;
|
||||||
color:var(--text-primary);
|
color: #000;
|
||||||
opacity: 1;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .step.active .step-number {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-text {
|
.step-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-primary);
|
color: #000000b5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.active .step-text {
|
.step.active .step-text {
|
||||||
color:var(--text-primary);
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-connector {
|
.step-connector {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
background-color: var(--accent-color);
|
background-color: #00000070;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 46px;
|
top: 46px;
|
||||||
|
|
@ -191,7 +118,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.active .step-connector {
|
.step.active .step-connector {
|
||||||
background-color:var(--text-primary);
|
background-color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main Content Styles */
|
/* Main Content Styles */
|
||||||
|
|
@ -201,17 +128,15 @@ body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: var(--bg-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-progress {
|
.mobile-progress {
|
||||||
background-color: var(--bg-secondary);
|
background-color: #f4f4f5;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-progress ol {
|
.mobile-progress ol {
|
||||||
|
|
@ -236,12 +161,11 @@ body {
|
||||||
.form-card {
|
.form-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
background: var(--bg-card);
|
background: white;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-title {
|
.form-title {
|
||||||
|
|
@ -249,7 +173,7 @@ body {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
color:var(--text-primary);
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-step {
|
.form-step {
|
||||||
|
|
@ -267,17 +191,10 @@ body {
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--bg-secondary);
|
background-color: #f9fafb;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
border: 1px solid transparent;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container:focus-within {
|
|
||||||
border-color: var(--accent-light);
|
|
||||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-label {
|
.input-label {
|
||||||
|
|
@ -286,7 +203,7 @@ body {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-secondary);
|
color: #6b7280;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
@ -299,16 +216,6 @@ body {
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--text-primary);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .input-field, [data-theme="dark"] .select-field {
|
|
||||||
background-color: #ffffff1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field::placeholder {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-field {
|
.select-field {
|
||||||
|
|
@ -320,10 +227,6 @@ body {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .select-field {
|
|
||||||
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='%23d1d5db' 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field:focus + .input-label,
|
.input-field:focus + .input-label,
|
||||||
.input-field:not(:placeholder-shown) + .input-label,
|
.input-field:not(:placeholder-shown) + .input-label,
|
||||||
.select-field:focus + .input-label,
|
.select-field:focus + .input-label,
|
||||||
|
|
@ -331,7 +234,6 @@ body {
|
||||||
top: 2px;
|
top: 2px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
transform: none;
|
transform: none;
|
||||||
color: var(--accent-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-group {
|
.checkbox-group {
|
||||||
|
|
@ -344,13 +246,10 @@ body {
|
||||||
.checkbox-group input {
|
.checkbox-group input {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
accent-color: var(--accent-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-group label {
|
.checkbox-group label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-primary);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
|
|
@ -370,33 +269,31 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: var(--accent-color);
|
background-color: #000000;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background-color: var(--bg-secondary);
|
background-color: #f3f4f6;
|
||||||
color: var(--text-primary);
|
color: #374151;
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background-color: var(--border-color);
|
background-color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid var(--accent-light);
|
border: 1px solid #070065;
|
||||||
color: var(--accent-light);
|
color: #070065;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline:hover {
|
.btn-outline:hover {
|
||||||
background-color: var(--accent-light);
|
background-color: #070065;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -404,7 +301,6 @@ body {
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
transform: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-sm {
|
.btn-sm {
|
||||||
|
|
@ -421,109 +317,31 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-option.selected {
|
.payment-option.selected {
|
||||||
border-color: var(--border-color);
|
border-color: #000000;
|
||||||
transition: all 0.3s ease;
|
background-color: #f0f9ff;
|
||||||
background: var(--bg-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-option:hover {
|
|
||||||
border-color: var(--accent-light);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-option.selected {
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
background-color: rgba(99, 102, 241, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .payment-option.selected {
|
|
||||||
background-color: rgba(99, 102, 241, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Dark Mode Styles */
|
|
||||||
[data-theme="dark"] .btn-primary {
|
|
||||||
background-color: #6366f1;
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .btn-primary:hover {
|
|
||||||
background-color: #4f46e5;
|
|
||||||
border-color: #4f46e5;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .btn-secondary {
|
|
||||||
background-color: #374151;
|
|
||||||
color: #d1d5db;
|
|
||||||
border: 1px solid #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .btn-secondary:hover {
|
|
||||||
background-color: #4b5563;
|
|
||||||
border-color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .btn-outline {
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid #6366f1;
|
|
||||||
color: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .btn-outline:hover {
|
|
||||||
background-color: #6366f1;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .btn:disabled {
|
|
||||||
background-color: #374151;
|
|
||||||
color: #6b7280;
|
|
||||||
border-color: #4b5563;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action button dark mode styles */
|
|
||||||
[data-theme="dark"] .password-toggle,
|
|
||||||
[data-theme="dark"] .password-generate,
|
|
||||||
[data-theme="dark"] .detect-location {
|
|
||||||
color: #818cf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .password-toggle:hover,
|
|
||||||
[data-theme="dark"] .password-generate:hover,
|
|
||||||
[data-theme="dark"] .detect-location:hover {
|
|
||||||
color: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus states for dark mode */
|
|
||||||
[data-theme="dark"] .btn:focus-visible {
|
|
||||||
outline: 2px solid #818cf8;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-option h4 {
|
.payment-option h4 {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-option p {
|
.payment-option p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-secondary);
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-details {
|
.payment-details {
|
||||||
display: none;
|
display: none;
|
||||||
margin: 4px 0 16px;
|
margin: 4px 0 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.3s ease;
|
background-color: #bdbdbd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-details.active {
|
.payment-details.active {
|
||||||
display: block;
|
display: block;
|
||||||
animation: slideDown 0.3s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-message {
|
.success-message {
|
||||||
|
|
@ -533,25 +351,24 @@ body {
|
||||||
|
|
||||||
.success-icon {
|
.success-icon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
color: var(--success-color);
|
color: #10b981;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-message h3 {
|
.success-message h3 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-message p {
|
.success-message p {
|
||||||
color: var(--text-secondary);
|
color: #6b7280;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-loading {
|
.location-loading {
|
||||||
display: none;
|
display: none;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: #6b7280;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -568,17 +385,17 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-incomplete {
|
.profile-incomplete {
|
||||||
background: var(--warning-bg);
|
background: #fff3cd;
|
||||||
border: 1px solid var(--warning-border);
|
border: 1px solid #ffeaa7;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
color: var(--warning-text);
|
color: #856404;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-strength {
|
.password-strength {
|
||||||
height: 5px;
|
height: 5px;
|
||||||
background: var(--bg-secondary);
|
background: #e9ecef;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -592,7 +409,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-weak {
|
.password-weak {
|
||||||
background: var(--error-color);
|
background: #dc3545;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -602,7 +419,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-strong {
|
.password-strong {
|
||||||
background: var(--success-color);
|
background: #28a745;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -613,134 +430,38 @@ body {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-toggle, .password-generate, .detect-location {
|
.password-toggle {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--accent-light);
|
color: #070065;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-toggle:hover, .password-generate:hover, .detect-location:hover {
|
.password-generate {
|
||||||
color: var(--accent-color);
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #070065;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-text {
|
.form-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: #6b7280;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error States */
|
.detect-location {
|
||||||
.step-error-container {
|
background: none;
|
||||||
background-color: rgba(220, 38, 38, 0.1);
|
border: none;
|
||||||
border: 1px solid rgba(220, 38, 38, 0.3);
|
color: #070065;
|
||||||
border-radius: 8px;
|
cursor: pointer;
|
||||||
padding: 12px 16px;
|
font-size: 12px;
|
||||||
margin-bottom: 20px;
|
text-decoration: underline;
|
||||||
animation: slideDown 0.3s ease-out;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .step-error-container {
|
|
||||||
background-color: rgba(220, 38, 38, 0.2);
|
|
||||||
border-color: rgba(220, 38, 38, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: var(--error-color);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message::before {
|
|
||||||
content: "⚠";
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-error {
|
|
||||||
border: 1px solid var(--error-color) !important;
|
|
||||||
background-color: rgba(220, 38, 38, 0.05) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .field-error {
|
|
||||||
background-color: rgba(220, 38, 38, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-heading {
|
|
||||||
margin-top: 24px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes slideDown {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar Styling */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] ::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:focus-visible,
|
|
||||||
.input-field:focus-visible,
|
|
||||||
.select-field:focus-visible,
|
|
||||||
.payment-option:focus-visible {
|
|
||||||
outline: 2px solid var(--accent-light);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selection Color */
|
|
||||||
::selection {
|
|
||||||
background-color: rgba(99, 102, 241, 0.3);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Responsive styles */
|
/* Responsive styles */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|
@ -756,35 +477,41 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
.step-error-container {
|
||||||
[data-theme="dark"] .mobile-progress {
|
background-color: #fef2f2;
|
||||||
background-color: var(--bg-card);
|
border: 1px solid #fecaca;
|
||||||
border: 1px solid var(--border-dark);
|
border-radius: 8px;
|
||||||
}
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
.mobile-progress .step {
|
animation: slideDown 0.3s ease-out;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .mobile-progress .step {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
.error-message {
|
||||||
[data-theme="dark"] {
|
color: #dc2626;
|
||||||
--bg-primary: #ffffff;
|
font-size: 14px;
|
||||||
--bg-card: #ffffff;
|
font-weight: 500;
|
||||||
--text-primary: #000000;
|
display: flex;
|
||||||
--text-secondary: #333333;
|
align-items: center;
|
||||||
--border-color: #cccccc;
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message::before {
|
||||||
|
content: "⚠";
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-error {
|
||||||
|
border: 1px solid #dc2626 !important;
|
||||||
|
background-color: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
|
to {
|
||||||
.sidebar {
|
opacity: 1;
|
||||||
display: none;
|
transform: translateY(0);
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 435 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" zoomAndPan="magnify" viewBox="0 0 600 599.999999" height="800" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="256ef070a6"><path d="M 23.410156 9.867188 L 578 9.867188 L 578 588 L 23.410156 588 Z M 23.410156 9.867188 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#256ef070a6)"><path fill="#060165" d="M 287.648438 9.867188 L 23.410156 9.867188 L 23.410156 321.480469 C 23.410156 468.195312 142.195312 587.019531 288.859375 587.019531 C 447.644531 587.019531 577.339844 457.28125 577.339844 298.445312 C 577.339844 139.605469 447.644531 9.867188 287.648438 9.867188 " fill-opacity="1" fill-rule="nonzero"/></g><path fill="#ffffff" d="M 237.949219 186.894531 C 237.949219 217.207031 212.496094 242.667969 182.195312 242.667969 C 151.890625 242.667969 126.4375 268.132812 126.4375 298.445312 C 126.4375 328.757812 151.890625 354.21875 182.195312 354.21875 L 293.707031 354.21875 C 324.011719 354.21875 349.464844 328.757812 349.464844 298.445312 L 349.464844 186.894531 C 349.464844 156.582031 324.011719 131.117188 293.707031 131.117188 C 263.40625 131.117188 237.949219 156.582031 237.949219 186.894531 " fill-opacity="1" fill-rule="nonzero"/><path fill="#05c3ec" d="M 361.585938 409.992188 C 361.585938 379.679688 387.039062 354.21875 417.34375 354.21875 C 447.644531 354.21875 473.097656 328.757812 473.097656 298.445312 C 473.097656 268.132812 447.644531 242.667969 417.34375 242.667969 L 305.828125 242.667969 C 275.527344 242.667969 250.070312 268.132812 250.070312 298.445312 L 250.070312 409.992188 C 250.070312 440.304688 275.527344 465.769531 305.828125 465.769531 C 336.132812 465.769531 361.585938 440.304688 361.585938 409.992188 " fill-opacity="1" fill-rule="nonzero"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,29 +0,0 @@
|
||||||
window.getAuthToken = function () {
|
|
||||||
return localStorage.getItem("knoptpq");
|
|
||||||
};
|
|
||||||
|
|
||||||
window.fetchWithAuth = async function (url, options = {}) {
|
|
||||||
const token = getAuthToken();
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/client/login";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
token,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
localStorage.clear();
|
|
||||||
window.location.href = "/client/login";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const profileTrigger = document.querySelector('.profile-trigger');
|
|
||||||
const profileMenu = document.querySelector('.profile-menu');
|
|
||||||
|
|
||||||
if (!profileTrigger || !profileMenu) return;
|
|
||||||
|
|
||||||
// Toggle on click
|
|
||||||
profileTrigger.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
profileMenu.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close on outside click
|
|
||||||
document.addEventListener('click', () => {
|
|
||||||
profileMenu.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prevent closing when clicking inside menu
|
|
||||||
profileMenu.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close on ESC
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
profileMenu.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const mobileMenuButton = document.querySelector(".mobile-menu-button");
|
|
||||||
const sidebar = document.querySelector(".dashboard-sidebar");
|
|
||||||
mobileMenuButton.addEventListener("click", () => {
|
|
||||||
sidebar.classList.toggle("active");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
function openLogoutModal() {
|
|
||||||
document.getElementById("logoutModal").style.display = "flex";
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeLogoutModal() {
|
|
||||||
document.getElementById("logoutModal").style.display = "none";
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLogout() {
|
|
||||||
try {
|
|
||||||
await fetchWithAuth("/api/v1/client/logout", { method: "POST" });
|
|
||||||
} finally {
|
|
||||||
localStorage.clear();
|
|
||||||
window.location.href = "/client/login";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
document.getElementById("logoutButton")?.addEventListener("click", openLogoutModal);
|
|
||||||
document.getElementById("confirmLogoutBtn")?.addEventListener("click", handleLogout);
|
|
||||||
});
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
document.querySelectorAll('.nav-button').forEach(link => {
|
|
||||||
if (link.getAttribute('href') === currentPath) {
|
|
||||||
link.classList.add('active');
|
|
||||||
} else {
|
|
||||||
link.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
||||||
if (link.getAttribute('href') === currentPath) {
|
|
||||||
link.classList.add('active');
|
|
||||||
} else {
|
|
||||||
link.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auth helpers
|
|
||||||
function getAuthToken() {
|
|
||||||
return localStorage.getItem("knoptpq");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithAuth(url, options = {}) {
|
|
||||||
const token = getAuthToken();
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/client/login";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"token": token,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
localStorage.clear();
|
|
||||||
window.location.href = "/client/login";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout modal
|
|
||||||
function openLogoutModal() {
|
|
||||||
document.getElementById("logoutModal").style.display = "flex";
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeLogoutModal() {
|
|
||||||
document.getElementById("logoutModal").style.display = "none";
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final logout
|
|
||||||
async function handleLogout() {
|
|
||||||
try {
|
|
||||||
await fetchWithAuth("/api/v1/client/logout", { method: "POST" });
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Logout error", e);
|
|
||||||
} finally {
|
|
||||||
localStorage.clear();
|
|
||||||
window.location.href = "/client/login";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOM bindings
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
|
|
||||||
// Logout button
|
|
||||||
const logoutBtn = document.getElementById("logoutButton");
|
|
||||||
if (logoutBtn) logoutBtn.addEventListener("click", openLogoutModal);
|
|
||||||
|
|
||||||
// Confirm logout
|
|
||||||
const confirmBtn = document.getElementById("confirmLogoutBtn");
|
|
||||||
if (confirmBtn) confirmBtn.addEventListener("click", handleLogout);
|
|
||||||
|
|
||||||
// Click outside modal
|
|
||||||
const modal = document.getElementById("logoutModal");
|
|
||||||
if (modal) {
|
|
||||||
modal.addEventListener("click", (e) => {
|
|
||||||
if (e.target === modal) closeLogoutModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
(function () {
|
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
function applyTheme(theme) {
|
|
||||||
root.setAttribute("data-theme", theme);
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemTheme() {
|
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
||||||
? "dark"
|
|
||||||
: "light";
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTheme() {
|
|
||||||
const savedTheme = localStorage.getItem("theme");
|
|
||||||
applyTheme(savedTheme || getSystemTheme());
|
|
||||||
}
|
|
||||||
|
|
||||||
// expose globally
|
|
||||||
window.setTheme = applyTheme;
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", initTheme);
|
|
||||||
})();
|
|
||||||
|
|
@ -1,263 +0,0 @@
|
||||||
const API_BASE = "/api/v1/client";
|
|
||||||
|
|
||||||
// Format time function
|
|
||||||
function formatTime(seconds) {
|
|
||||||
if (!seconds) return 'No Record Found';
|
|
||||||
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes}m`;
|
|
||||||
} else if (minutes > 0) {
|
|
||||||
return `${minutes}m`;
|
|
||||||
} else {
|
|
||||||
return 'Just started';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const vaModal = document.getElementById('vaModal');
|
|
||||||
|
|
||||||
function openVAModal(va) {
|
|
||||||
// Set basic VA info
|
|
||||||
document.getElementById('modalVAName').textContent = va.name;
|
|
||||||
|
|
||||||
// Set projects
|
|
||||||
const projects = va.projects || [];
|
|
||||||
document.getElementById('projectsCountBadge').textContent = projects.length;
|
|
||||||
|
|
||||||
const projectsTableBody = document.getElementById('modalProjectsTableBody');
|
|
||||||
const noProjectsMessage = document.getElementById('modalNoProjects');
|
|
||||||
|
|
||||||
if (projects.length > 0) {
|
|
||||||
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
|
|
||||||
projectsTableBody.style.display = '';
|
|
||||||
noProjectsMessage.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
projectsTableBody.innerHTML = '';
|
|
||||||
projectsTableBody.style.display = 'none';
|
|
||||||
noProjectsMessage.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set action button
|
|
||||||
const modalActions = document.getElementById('modalActions');
|
|
||||||
const viewSessionBtn = document.getElementById('viewSessionBtn');
|
|
||||||
|
|
||||||
if (va.isCurrentlyActive && va.lastProjectId) {
|
|
||||||
viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
|
|
||||||
modalActions.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
modalActions.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show modal
|
|
||||||
vaModal.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeVAModal() {
|
|
||||||
vaModal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create project table row for modal
|
|
||||||
function createProjectTableRow(project) {
|
|
||||||
const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
|
|
||||||
const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="project-info">
|
|
||||||
<div class="project-name">${project.name}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="hourly-rate">$${project.hourlyRate}/hr</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="worked-time">${project.totalWorkedTime}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="status-badge ${statusClass}">
|
|
||||||
<span class="status-text">${statusText}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create VA table row for main table
|
|
||||||
function createVATableRow(va) {
|
|
||||||
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
|
|
||||||
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
|
|
||||||
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
|
|
||||||
const workedTime = formatTime(va.currentSessionDuration);
|
|
||||||
const projectsAssigned = va.projectsAssigned || 0;
|
|
||||||
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : `<a class="va-logs va-action" data-va-id="${va.id}" target="_blank" href="/client/virtual-assistants/view-logs?va=${va.id}">Logs</a>`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr class="va-row" data-va-id="${va.id}">
|
|
||||||
<td>
|
|
||||||
<div class="status-indicator">
|
|
||||||
<span class="status-text ${statusClass}">${statusText}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="va-info">
|
|
||||||
<div class="va-name">${va.name}</div>
|
|
||||||
<div class="va-email">${va.email || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="projects-count">${activeProject}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="projects-count">${projectsAssigned} Projects</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="worked-time">${workedTime}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
${viewLink}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load dashboard data
|
|
||||||
async function loadDashboardData() {
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
|
||||||
if (!response) return;
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
const data = result.data;
|
|
||||||
const summary = result.summary;
|
|
||||||
|
|
||||||
// Update stats cards
|
|
||||||
document.getElementById('activeVaCount').textContent = summary.activeVAs || 0;
|
|
||||||
document.getElementById('totalVaCount').textContent = summary.totalVAs || 0;
|
|
||||||
document.getElementById('totalProjects').textContent = summary.totalProjects || 0;
|
|
||||||
|
|
||||||
// Update VA list count
|
|
||||||
document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`;
|
|
||||||
|
|
||||||
// Update VA table
|
|
||||||
const vaTableBody = document.getElementById('vaTableBody');
|
|
||||||
const vaEmptyState = document.getElementById('vaEmptyState');
|
|
||||||
const allVAs = data.allVAs || [];
|
|
||||||
|
|
||||||
if (allVAs.length > 0) {
|
|
||||||
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
|
|
||||||
vaTableBody.style.display = '';
|
|
||||||
vaEmptyState.style.display = 'none';
|
|
||||||
|
|
||||||
// Store VA data globally for modal access
|
|
||||||
window.vaData = allVAs;
|
|
||||||
|
|
||||||
// Add click event listeners to VA rows
|
|
||||||
document.querySelectorAll('.va-row').forEach((row, index) => {
|
|
||||||
row.addEventListener('click', function (e) {
|
|
||||||
// Don't open modal if clicking on view link
|
|
||||||
if (!e.target.closest('.view-link')) {
|
|
||||||
const va = allVAs[index];
|
|
||||||
openVAModal(va);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
vaTableBody.innerHTML = '';
|
|
||||||
vaTableBody.style.display = 'none';
|
|
||||||
vaEmptyState.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error('Failed to load dashboard data:', result.message);
|
|
||||||
showError('Failed to load dashboard data');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading dashboard data:', error);
|
|
||||||
showError('Error loading dashboard data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message
|
|
||||||
function showError(message) {
|
|
||||||
const vaTableBody = document.getElementById('vaTableBody');
|
|
||||||
const vaEmptyState = document.getElementById('vaEmptyState');
|
|
||||||
|
|
||||||
vaTableBody.style.display = 'none';
|
|
||||||
vaEmptyState.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
<h3>Error</h3>
|
|
||||||
<p>${message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
vaEmptyState.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close modal when clicking outside
|
|
||||||
vaModal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === vaModal) {
|
|
||||||
closeVAModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && vaModal.style.display === 'flex') {
|
|
||||||
closeVAModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optional: Auto-refresh data every 30 seconds
|
|
||||||
setInterval(loadDashboardData, 30000);
|
|
||||||
|
|
||||||
// Logout Modal Functions
|
|
||||||
function openLogoutModal() {
|
|
||||||
const modal = document.getElementById('logoutModal');
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeLogoutModal() {
|
|
||||||
const modal = document.getElementById('logoutModal');
|
|
||||||
modal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Load dashboard data
|
|
||||||
loadDashboardData();
|
|
||||||
|
|
||||||
// Mobile menu toggle
|
|
||||||
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
||||||
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
||||||
menuToggle.addEventListener('click', () => {
|
|
||||||
sidebar.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout button - opens confirmation modal instead of directly logging out
|
|
||||||
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
||||||
|
|
||||||
// Confirm logout button
|
|
||||||
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
const API_BASE = "/api/v1/client";
|
|
||||||
|
|
||||||
/* ------------------ Utils ------------------ */
|
|
||||||
|
|
||||||
function formatTime(seconds) {
|
|
||||||
if (!seconds) return "No Record";
|
|
||||||
const h = Math.floor(seconds / 3600);
|
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
|
||||||
return h ? `${h}h ${m}m` : `${m}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------ DOM Elements ------------------ */
|
|
||||||
|
|
||||||
const vaModal = document.getElementById("vaModal");
|
|
||||||
const modalVAName = document.getElementById("modalVAName");
|
|
||||||
const modalProjectsTableBody = document.getElementById("modalProjectsTableBody");
|
|
||||||
const modalNoProjects = document.getElementById("modalNoProjects");
|
|
||||||
|
|
||||||
const vaTableBody = document.getElementById("vaTableBody");
|
|
||||||
const vaEmptyState = document.getElementById("vaEmptyState");
|
|
||||||
|
|
||||||
/* ------------------ Modal Controls ------------------ */
|
|
||||||
|
|
||||||
function openVAModal(va) {
|
|
||||||
modalVAName.textContent = va.name || "VA Details";
|
|
||||||
|
|
||||||
const projects = va.projects || [];
|
|
||||||
|
|
||||||
if (projects.length === 0) {
|
|
||||||
modalProjectsTableBody.innerHTML = "";
|
|
||||||
modalNoProjects.style.display = "block";
|
|
||||||
} else {
|
|
||||||
modalNoProjects.style.display = "none";
|
|
||||||
modalProjectsTableBody.innerHTML = projects
|
|
||||||
.map(createProjectRow)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
vaModal.style.display = "flex";
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeVAModal() {
|
|
||||||
vaModal.style.display = "none";
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Outside click close */
|
|
||||||
vaModal.addEventListener("click", (e) => {
|
|
||||||
if (e.target === vaModal) closeVAModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ESC key close */
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Escape") closeVAModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ------------------ Table Builders ------------------ */
|
|
||||||
|
|
||||||
function createProjectRow(project) {
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<td>${project.name}</td>
|
|
||||||
<td>$${project.hourlyRate}/hr</td>
|
|
||||||
<td>${project.totalWorkedTime}</td>
|
|
||||||
<td>${project.isCurrentlyWorking ? "Working" : "Inactive"}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVATableRow(va, index) {
|
|
||||||
return `
|
|
||||||
<tr class="va-row" data-index="${index}">
|
|
||||||
<td>
|
|
||||||
<span class="status ${va.isCurrentlyActive ? "active" : "inactive"}">
|
|
||||||
${va.isCurrentlyActive ? "Active" : "Inactive"}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>${va.name}</td>
|
|
||||||
<td>${va.lastProject?.name || "-"}</td>
|
|
||||||
<td>${va.projectsAssigned || 0}</td>
|
|
||||||
<td>${va.currentSessionDuration || "No Record Found"}</td>
|
|
||||||
<td>
|
|
||||||
${
|
|
||||||
va.isCurrentlyActive
|
|
||||||
? `<a href="/live/${va.lastProjectId}/${va.id}" target="_blank">Live</a>`
|
|
||||||
: "-"
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setInterval(loadDashboardData, 30000);
|
|
||||||
|
|
||||||
/* ------------------ Load Dashboard ------------------ */
|
|
||||||
|
|
||||||
async function loadDashboardData() {
|
|
||||||
const res = await fetchWithAuth(`${API_BASE}/dashboard`);
|
|
||||||
if (!res) return;
|
|
||||||
|
|
||||||
const result = await res.json();
|
|
||||||
if (!result.success) return;
|
|
||||||
|
|
||||||
const { summary, data } = result;
|
|
||||||
|
|
||||||
document.getElementById("activeVaCount").textContent = summary.activeVAs;
|
|
||||||
document.getElementById("totalVaCount").textContent = summary.totalVAs;
|
|
||||||
document.getElementById("totalProjects").textContent = summary.totalProjects;
|
|
||||||
document.getElementById("vaListCount").textContent = `${summary.totalVAs} VAs`;
|
|
||||||
|
|
||||||
const allVAs = data.allVAs || [];
|
|
||||||
|
|
||||||
if (allVAs.length === 0) {
|
|
||||||
vaTableBody.innerHTML = "";
|
|
||||||
vaEmptyState.style.display = "block";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
vaEmptyState.style.display = "none";
|
|
||||||
|
|
||||||
/* store globally for modal */
|
|
||||||
window.allVAs = allVAs;
|
|
||||||
|
|
||||||
vaTableBody.innerHTML = allVAs
|
|
||||||
.map((va, index) => createVATableRow(va, index))
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
/* Row click handlers */
|
|
||||||
document.querySelectorAll(".va-row").forEach(row => {
|
|
||||||
row.addEventListener("click", (e) => {
|
|
||||||
if (e.target.tagName === "A") return; // ignore Live click
|
|
||||||
const index = row.dataset.index;
|
|
||||||
openVAModal(window.allVAs[index]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------ Init ------------------ */
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", loadDashboardData);
|
|
||||||
|
|
@ -1,248 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
// API base URL
|
|
||||||
const API_BASE = '/api/v1/client';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function openTipModal() {
|
|
||||||
const modal = document.getElementById('tipModal');
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
|
|
||||||
// Clear form
|
|
||||||
document.getElementById('tipForm').reset();
|
|
||||||
clearErrorMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTipModal() {
|
|
||||||
const modal = document.getElementById('tipModal');
|
|
||||||
modal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
|
|
||||||
// Clear form and errors
|
|
||||||
document.getElementById('tipForm').reset();
|
|
||||||
clearErrorMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearErrorMessages() {
|
|
||||||
document.getElementById('vaForTipError').textContent = '';
|
|
||||||
document.getElementById('vaTipAmountError').textContent = '';
|
|
||||||
document.getElementById('vaTipDescriptionError').textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
|
|
||||||
// Mobile menu toggle
|
|
||||||
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
||||||
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
||||||
menuToggle.addEventListener('click', () => {
|
|
||||||
sidebar.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal);
|
|
||||||
|
|
||||||
document.getElementById('tipModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeTipModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeLogoutModal();
|
|
||||||
closeTipModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout button - opens confirmation modal instead of directly logging out
|
|
||||||
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
||||||
|
|
||||||
// Confirm logout button
|
|
||||||
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ------------------------------
|
|
||||||
// TIP FORM SUBMISSION HANDLER
|
|
||||||
// ------------------------------
|
|
||||||
|
|
||||||
document.getElementById('tipForm').addEventListener('submit', async function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// clearErrorMessages();
|
|
||||||
|
|
||||||
const vaId = document.getElementById('vaForTip').value;
|
|
||||||
const amount = document.getElementById('vaTipAmount').value.trim();
|
|
||||||
const description = document.getElementById('vaTipDescription').value.trim();
|
|
||||||
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!vaId) {
|
|
||||||
document.getElementById('vaForTipError').textContent = "Please select a virtual assistant.";
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!amount || isNaN(amount) || Number(amount) <= 0) {
|
|
||||||
document.getElementById('vaTipAmountError').textContent = "Enter a valid tip amount.";
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
// Prepare API body
|
|
||||||
const payload = {
|
|
||||||
vaId,
|
|
||||||
amount: Number(amount),
|
|
||||||
description
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.getElementById('submitTipBtn').disabled = true;
|
|
||||||
document.getElementById('submitTipBtn').textContent = "Submitting...";
|
|
||||||
|
|
||||||
const response = await fetchWithAuth(`/api/v1/client/tips`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
alert(data.message || "Failed to submit tip.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert("Tip submitted successfully!");
|
|
||||||
closeTipModal();
|
|
||||||
|
|
||||||
// Optionally refresh page or reload tips list
|
|
||||||
window.location.reload();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Tip submit error:", err);
|
|
||||||
alert("Something went wrong while submitting tip.");
|
|
||||||
} finally {
|
|
||||||
document.getElementById('submitTipBtn').disabled = false;
|
|
||||||
document.getElementById('submitTipBtn').textContent = "Submit Tip";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Load VAs for tip selection
|
|
||||||
async function loadVAs() {
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("Failed to load VAs:", data.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vaSelect = document.getElementById('vaForTip');
|
|
||||||
data.data.allVAs.forEach(va => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = va.id;
|
|
||||||
option.textContent = va.name;
|
|
||||||
vaSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading VAs:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadVAs();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Load VAs for tip selection
|
|
||||||
async function loadTips() {
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/tips`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("Failed to load VAs:", data.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const vaSelect = document.getElementById('vaTipTableBody');
|
|
||||||
data.data.forEach(tip => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
const cellVoid = document.createElement('td');
|
|
||||||
const cellStatus = document.createElement('td');
|
|
||||||
const cellName = document.createElement('td');
|
|
||||||
const cellDescription = document.createElement('td');
|
|
||||||
const cellDate = document.createElement('td');
|
|
||||||
const cellAmount = document.createElement('td');
|
|
||||||
|
|
||||||
cellVoid.innerHTML = `<button id='voidBtn' class='void-btn-a' data-id='${tip._id}'>Void</button>`;
|
|
||||||
cellVoid.classList.add('void-btn');
|
|
||||||
|
|
||||||
cellStatus.textContent = tip.status;
|
|
||||||
cellName.textContent = tip.vaName;
|
|
||||||
cellDescription.textContent = tip.description;
|
|
||||||
cellDate.textContent = tip.createdAt ? new Date(tip.createdAt).toLocaleDateString() : '';
|
|
||||||
cellAmount.textContent = `$${parseFloat(tip.amount).toFixed(2)}`;
|
|
||||||
|
|
||||||
row.appendChild(cellVoid);
|
|
||||||
row.appendChild(cellStatus);
|
|
||||||
row.appendChild(cellName);
|
|
||||||
row.appendChild(cellDescription);
|
|
||||||
row.appendChild(cellDate);
|
|
||||||
row.appendChild(cellAmount);
|
|
||||||
|
|
||||||
vaSelect.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.void-btn-a').forEach(button => {
|
|
||||||
button.addEventListener('click', async function () {
|
|
||||||
const tipId = this.getAttribute('data-id');
|
|
||||||
if (confirm('Are you sure you want to void this tip?')) {
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/tips/${tipId}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
alert(result.message || 'Failed to void tip.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert('Tip voided successfully!');
|
|
||||||
window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error voiding tip:', error);
|
|
||||||
alert('Something went wrong while voiding the tip.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading VAs:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTips();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,240 +0,0 @@
|
||||||
// API base URL
|
|
||||||
const API_BASE = '/api/v1/client';
|
|
||||||
|
|
||||||
// Modal management
|
|
||||||
const projectModal = document.getElementById('projectModal');
|
|
||||||
const modalClose = document.getElementById('modalClose');
|
|
||||||
const modalProjectName = document.getElementById('modalProjectName');
|
|
||||||
const modalVATableBody = document.getElementById('modalVATableBody');
|
|
||||||
const modalNoVAs = document.getElementById('modalNoVAs');
|
|
||||||
const vaCountBadge = document.getElementById('vaCountBadge');
|
|
||||||
|
|
||||||
|
|
||||||
function openProjectModal(project) {
|
|
||||||
// Set project name
|
|
||||||
modalProjectName.textContent = project.name || 'Unnamed Project';
|
|
||||||
|
|
||||||
// Set project info
|
|
||||||
const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : 'Not set';
|
|
||||||
const dedicatedHours = project.dedicatedHours || 'Not set';
|
|
||||||
const statusText = project.status === 'ACTIVE' ? 'Active' : 'Inactive';
|
|
||||||
const statusClass = project.status === 'ACTIVE' ? 'active' : 'inactive';
|
|
||||||
|
|
||||||
// Set VA details
|
|
||||||
const assignedVAs = project.assignedVADetails || [];
|
|
||||||
vaCountBadge.textContent = assignedVAs.length;
|
|
||||||
|
|
||||||
if (assignedVAs.length > 0) {
|
|
||||||
modalVATableBody.innerHTML = assignedVAs.map(va => createVATableRow(va)).join('');
|
|
||||||
modalVATableBody.style.display = '';
|
|
||||||
modalNoVAs.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
modalVATableBody.innerHTML = '';
|
|
||||||
modalVATableBody.style.display = 'none';
|
|
||||||
modalNoVAs.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show modal
|
|
||||||
projectModal.style.display = 'block';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVATableRow(va) {
|
|
||||||
const statusClass = va.status === 'FULLTIME' ? 'active' : 'inactive';
|
|
||||||
const statusText = va.status === 'FULLTIME' ? 'Full Time' :
|
|
||||||
va.status === 'PARTTIME' ? 'Part Time' : 'Inactive';
|
|
||||||
|
|
||||||
const activityClass = va.isActive ? 'active' : 'inactive';
|
|
||||||
const activityText = va.isActive ? 'Currently Active' : 'Not Active';
|
|
||||||
|
|
||||||
// Get initials for avatar
|
|
||||||
const initials = (va.firstName?.charAt(0) || '') + (va.lastName?.charAt(0) || '') || 'VA';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="va-name">
|
|
||||||
<div class="va-avatar">${initials}</div>
|
|
||||||
<div class="va-details">
|
|
||||||
<span class="va-fullname">${va.firstName} ${va.lastName}</span>
|
|
||||||
<span class="va-email">${va.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class=" ${statusClass}">
|
|
||||||
${statusText}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="activity-status">
|
|
||||||
<div class="activity-dot ${activityClass}"></div>
|
|
||||||
<span>${activityText}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeProjectModal() {
|
|
||||||
projectModal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close modal when clicking outside
|
|
||||||
projectModal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === projectModal) {
|
|
||||||
closeProjectModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && projectModal.style.display === 'block') {
|
|
||||||
closeProjectModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function createProjectTableRow(project) {
|
|
||||||
const statusClass = project.status === 'ACTIVE' ? 'active' : 'inactive';
|
|
||||||
const statusText = project.status === 'ACTIVE' ? 'Active' : 'Inactive';
|
|
||||||
const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : '-';
|
|
||||||
const dedicatedHours = project.dedicatedHours ? `${project.dedicatedHours}` : '-';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr class="project-row" data-project-id="${project.id}">
|
|
||||||
<td>
|
|
||||||
<div class="project-info">
|
|
||||||
<div class="project-name">${project.name || 'Unnamed Project'}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="hourly-rate">${hourlyRate}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="va-count">${project.vaAssigned || 0}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="dedicated-hours">${dedicatedHours}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load dashboard data
|
|
||||||
async function loadDashboardData() {
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/dashboard-projects`);
|
|
||||||
if (!response) return;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
const data = result.data;
|
|
||||||
const counts = result.counts;
|
|
||||||
|
|
||||||
// Update counts
|
|
||||||
document.getElementById('projectsCount').textContent = `${counts.totalProjects || 0} Projects`;
|
|
||||||
|
|
||||||
// Update Projects table
|
|
||||||
const projectsTableBody = document.getElementById('projectsTableBody');
|
|
||||||
const projectsEmptyState = document.getElementById('projectsEmptyState');
|
|
||||||
const projects = data.projects || [];
|
|
||||||
|
|
||||||
if (projects.length > 0) {
|
|
||||||
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
|
|
||||||
projectsTableBody.style.display = '';
|
|
||||||
projectsEmptyState.style.display = 'none';
|
|
||||||
|
|
||||||
// Add click event listeners to project rows
|
|
||||||
document.querySelectorAll('.project-row').forEach((row, index) => {
|
|
||||||
row.addEventListener('click', function () {
|
|
||||||
const project = projects[index];
|
|
||||||
openProjectModal(project);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
projectsTableBody.innerHTML = '';
|
|
||||||
projectsTableBody.style.display = 'none';
|
|
||||||
projectsEmptyState.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error('Failed to load dashboard data:', result.message);
|
|
||||||
showError('Failed to load dashboard data');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading dashboard data:', error);
|
|
||||||
showError('Error loading dashboard data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message
|
|
||||||
function showError(message) {
|
|
||||||
const projectsTableBody = document.getElementById('projectsTableBody');
|
|
||||||
const projectsEmptyState = document.getElementById('projectsEmptyState');
|
|
||||||
const vaTableBody = document.getElementById('vaTableBody');
|
|
||||||
const vaEmptyState = document.getElementById('vaEmptyState');
|
|
||||||
|
|
||||||
// Hide tables
|
|
||||||
projectsTableBody.style.display = 'none';
|
|
||||||
vaTableBody.style.display = 'none';
|
|
||||||
|
|
||||||
// Show error states
|
|
||||||
projectsEmptyState.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
<h3>Error</h3>
|
|
||||||
<p>${message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
projectsEmptyState.style.display = 'block';
|
|
||||||
|
|
||||||
vaEmptyState.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
<h3>Error</h3>
|
|
||||||
<p>${message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
vaEmptyState.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Optional: Auto-refresh data every 30 seconds
|
|
||||||
setInterval(loadDashboardData, 30000);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Load dashboard data
|
|
||||||
loadDashboardData();
|
|
||||||
|
|
||||||
// Mobile menu toggle
|
|
||||||
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
||||||
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
||||||
menuToggle.addEventListener('click', () => {
|
|
||||||
sidebar.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout button - opens confirmation modal instead of directly logging out
|
|
||||||
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
||||||
|
|
||||||
// Confirm logout button
|
|
||||||
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
||||||
});
|
|
||||||
|
|
||||||
modalClose.addEventListener('click', closeProjectModal);
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
// API base URL
|
|
||||||
const API_BASE = '/api/v1/client';
|
|
||||||
|
|
||||||
const vaCreditsTableBody = document.getElementById("vaCreditTableBody");
|
|
||||||
const vaCreditsEmptyState = document.getElementById("vaEmptyCreditState");
|
|
||||||
|
|
||||||
function createCreditRow(credit) {
|
|
||||||
return `
|
|
||||||
<tr data-credit-id="${credit.id}">
|
|
||||||
<td><span class="status ${credit.status.toLowerCase()}">${credit.status}</span></td>
|
|
||||||
<td>${credit.date}</td>
|
|
||||||
<td>${credit.comment}</td>
|
|
||||||
<td>$${credit.creditAmount}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function loadCreditsData() {
|
|
||||||
const res = await fetchWithAuth(`${API_BASE}/credits`);
|
|
||||||
if (!res) return;
|
|
||||||
|
|
||||||
const result = await res.json();
|
|
||||||
if (!result.success) return;
|
|
||||||
console.log("Credits Result:", result);
|
|
||||||
|
|
||||||
const { data } = result;
|
|
||||||
|
|
||||||
console.log("Credits Data:", data);
|
|
||||||
|
|
||||||
const allCredits = data || [];
|
|
||||||
|
|
||||||
if (allCredits.length === 0) {
|
|
||||||
vaCreditsTableBody.innerHTML = "";
|
|
||||||
vaCreditsEmptyState.style.display = "block";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
vaCreditsEmptyState.style.display = "none";
|
|
||||||
|
|
||||||
vaCreditsTableBody.innerHTML = allCredits
|
|
||||||
.map((credit) => createCreditRow(credit))
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
let totalCredits = 0;
|
|
||||||
let availableCredits = 0;
|
|
||||||
|
|
||||||
allCredits.forEach(credit => {
|
|
||||||
totalCredits += parseFloat(credit.creditAmount);
|
|
||||||
if (credit.status.toLowerCase() === 'pending') {
|
|
||||||
availableCredits += parseFloat(credit.creditAmount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById("vaTotalCredits").textContent = `$${totalCredits}`;
|
|
||||||
document.getElementById("vaAvailableCredits").textContent = `$${availableCredits}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
loadCreditsData();
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
// API base URL
|
|
||||||
const API_BASE = '/api/v1/client';
|
|
||||||
|
|
||||||
function toggleAccordion(accordionId) {
|
|
||||||
const accordion = document.getElementById(accordionId);
|
|
||||||
accordion.classList.toggle('active');
|
|
||||||
|
|
||||||
// const allAccordions = document.querySelectorAll('.accordion');
|
|
||||||
// allAccordions.forEach(acc => {
|
|
||||||
// if (acc.id !== accordionId && acc.classList.contains('active')) {
|
|
||||||
// acc.classList.remove('active');
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(milliseconds) {
|
|
||||||
if (!milliseconds) return '0m';
|
|
||||||
|
|
||||||
const seconds = Math.floor(milliseconds / 1000);
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes}m ${seconds % 60}s`;
|
|
||||||
}
|
|
||||||
return `${minutes}m ${seconds % 60}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(dateString) {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimeOnly(dateString) {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleTimeString('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateTotalIndividualLogsHours(workTrackLogs) {
|
|
||||||
let total = 0;
|
|
||||||
workTrackLogs.forEach(workTrack => {
|
|
||||||
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
|
||||||
workTrack.logs.forEach(log => {
|
|
||||||
total += log.totalTime || 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
function countIndividualLogs(workTrackLogs) {
|
|
||||||
let count = 0;
|
|
||||||
workTrackLogs.forEach(workTrack => {
|
|
||||||
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
|
||||||
count += workTrack.logs.length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateTotalHours(logs) {
|
|
||||||
return logs.reduce((total, log) => total + (log.totalTime || 0), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create project accordion
|
|
||||||
function createProjectAccordion(projectData, index) {
|
|
||||||
const workTrackLogs = projectData.workTrackLogs || [];
|
|
||||||
const totalHours = calculateTotalIndividualLogsHours(workTrackLogs);
|
|
||||||
const individualLogsCount = countIndividualLogs(workTrackLogs);
|
|
||||||
const projectName = projectData.projectDetails?.name || `Project ${index + 1}`;
|
|
||||||
|
|
||||||
const accordionId = `project-${index}`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="accordion" id="${accordionId}">
|
|
||||||
<div class="accordion-header" onclick="toggleAccordion('${accordionId}')">
|
|
||||||
<div class="accordion-title">
|
|
||||||
${projectName}
|
|
||||||
<span class="logs-count">${individualLogsCount} activities</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<div class="time-badge">${formatTime(totalHours)}</div>
|
|
||||||
</div>
|
|
||||||
<i class="fas fa-chevron-down accordion-icon"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="accordion-content">
|
|
||||||
${individualLogsCount > 0 ? createIndividualLogsTable(workTrackLogs) : createNoLogsMessage()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create table for individual log entries
|
|
||||||
function createIndividualLogsTable(workTrackLogs) {
|
|
||||||
// Collect all individual logs from all work tracks
|
|
||||||
const allIndividualLogs = [];
|
|
||||||
workTrackLogs.forEach(workTrack => {
|
|
||||||
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
|
||||||
workTrack.logs.forEach(log => {
|
|
||||||
allIndividualLogs.push(log);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>App Name</th>
|
|
||||||
<th>From</th>
|
|
||||||
<th>To</th>
|
|
||||||
<th>Total Time</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${allIndividualLogs.map((log, index) => createIndividualLogRow(log, index)).join('')}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a single row for individual log entry
|
|
||||||
function createIndividualLogRow(log, index) {
|
|
||||||
const title = log.title || `Activity ${index + 1}`;
|
|
||||||
const appName = log.appName || 'Unknown App';
|
|
||||||
|
|
||||||
// Parse dates
|
|
||||||
const fromDate = log.from;
|
|
||||||
const toDate = log.to || log.from;
|
|
||||||
|
|
||||||
// Calculate duration
|
|
||||||
let duration = log.totalTime || 0;
|
|
||||||
if (!duration && fromDate && toDate) {
|
|
||||||
const from = new Date(fromDate);
|
|
||||||
const to = new Date(toDate);
|
|
||||||
duration = Math.floor((to - from) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong>${title}</strong>
|
|
||||||
</td>
|
|
||||||
<td>${appName}</td>
|
|
||||||
<td>
|
|
||||||
<div class="datetime-cell">
|
|
||||||
<div class="date-text">${formatDate(fromDate)}</div>
|
|
||||||
<div class="time-text">${formatTimeOnly(fromDate)}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="datetime-cell">
|
|
||||||
<div class="date-text">${formatDate(toDate)}</div>
|
|
||||||
<div class="time-text">${formatTimeOnly(toDate)}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="time-badge">${formatTime(duration)}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create no logs message
|
|
||||||
function createNoLogsMessage() {
|
|
||||||
return `
|
|
||||||
<div style="text-align: center; padding: 2rem; color: #64748b;">
|
|
||||||
<i class="fas fa-clipboard"></i>
|
|
||||||
<h4>No Activity Logs</h4>
|
|
||||||
<p>No detailed activity logs available for this project.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load VA logs
|
|
||||||
async function loadVALogs() {
|
|
||||||
const loadingState = document.getElementById('loadingState');
|
|
||||||
const emptyState = document.getElementById('emptyState');
|
|
||||||
const projectsContainer = document.getElementById('projectsContainer');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get VA ID from URL
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const vaId = urlParams.get('va');
|
|
||||||
|
|
||||||
if (!vaId) {
|
|
||||||
window.location.href = '/client/virtual-assistants';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch VA logs data
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/vaLogsData/${vaId}`);
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
throw new Error('Failed to load VA logs');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!data.success) {
|
|
||||||
throw new Error(data.message || 'Failed to load VA logs');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide loading state
|
|
||||||
loadingState.style.display = 'none';
|
|
||||||
|
|
||||||
const projectLogs = data.data?.projectLogs || [];
|
|
||||||
|
|
||||||
// Handle empty state
|
|
||||||
if (projectLogs.length === 0) {
|
|
||||||
emptyState.style.display = 'block';
|
|
||||||
projectsContainer.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show projects container
|
|
||||||
projectsContainer.style.display = 'block';
|
|
||||||
emptyState.style.display = 'none';
|
|
||||||
|
|
||||||
// Create accordions for each project
|
|
||||||
let accordionsHTML = '';
|
|
||||||
projectLogs.forEach((project, index) => {
|
|
||||||
accordionsHTML += createProjectAccordion(project, index);
|
|
||||||
});
|
|
||||||
|
|
||||||
projectsContainer.innerHTML = accordionsHTML;
|
|
||||||
|
|
||||||
// Auto-open first accordion
|
|
||||||
if (projectLogs.length > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const firstAccordion = document.getElementById('project-0');
|
|
||||||
if (firstAccordion) {
|
|
||||||
firstAccordion.classList.add('active');
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading VA logs:", error);
|
|
||||||
loadingState.style.display = 'none';
|
|
||||||
projectsContainer.style.display = 'none';
|
|
||||||
|
|
||||||
// Show error in empty state
|
|
||||||
emptyState.innerHTML = `
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
<h3>Error Loading Data</h3>
|
|
||||||
<p>${error.message}</p>
|
|
||||||
<button onclick="loadVALogs()" class="btn btn-primary" style="margin-top: 1rem;">
|
|
||||||
<i class="fas fa-redo"></i> Retry
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
emptyState.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
|
|
||||||
// Mobile menu toggle
|
|
||||||
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
||||||
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
||||||
menuToggle.addEventListener('click', () => {
|
|
||||||
sidebar.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout button - opens confirmation modal instead of directly logging out
|
|
||||||
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
||||||
|
|
||||||
// Confirm logout button
|
|
||||||
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
||||||
|
|
||||||
// Load VA logs on page load
|
|
||||||
loadVALogs();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
@ -166,7 +166,7 @@ function validateCVV(cvv, cardType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZIP code validation
|
// ZIP code validation
|
||||||
function validateZipCodeRegex(zipCode) {
|
function validateZipCode(zipCode) {
|
||||||
return /^\d{5}(-\d{4})?$/.test(zipCode);
|
return /^\d{5}(-\d{4})?$/.test(zipCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -450,7 +450,7 @@ async function validateStep(step) {
|
||||||
|
|
||||||
// ZIP code validation
|
// ZIP code validation
|
||||||
const zipCode = document.getElementById('card-zip').value;
|
const zipCode = document.getElementById('card-zip').value;
|
||||||
if (!validateZipCodeRegex(zipCode)) {
|
if (!validateZipCode(zipCode)) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
showError('Please enter a valid ZIP code', 'card-zip', step);
|
showError('Please enter a valid ZIP code', 'card-zip', step);
|
||||||
return isValid;
|
return isValid;
|
||||||
|
|
@ -491,13 +491,10 @@ async function validateStep(step) {
|
||||||
}
|
}
|
||||||
} else if (step === 1) {
|
} else if (step === 1) {
|
||||||
const email = document.getElementById('email').value;
|
const email = document.getElementById('email').value;
|
||||||
const firstName = document.getElementById('first-name').value;
|
const companyName = document.getElementById('company-name').value;
|
||||||
const lastName = document.getElementById('last-name').value;
|
if (!validateEmail(email) && !companyName) {
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
|
||||||
if (!firstName && !lastName && !password && !confirmPassword) {
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
showError('Fields are Required', null, step);
|
showError('Both Fields are Required', null, step);
|
||||||
let requiredFields1 = form.querySelectorAll('[required]');
|
let requiredFields1 = form.querySelectorAll('[required]');
|
||||||
requiredFields1.forEach(field => {
|
requiredFields1.forEach(field => {
|
||||||
if (!field.value.trim()) {
|
if (!field.value.trim()) {
|
||||||
|
|
@ -519,48 +516,9 @@ async function validateStep(step) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!firstName) {
|
|
||||||
isValid = false;
|
|
||||||
showError('First Name is Required', 'first-name', step);
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
if (!lastName) {
|
|
||||||
isValid = false;
|
|
||||||
showError('Last Name is Required', 'last-name', step);
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 8) {
|
|
||||||
showError('Password must be at least 8 characters long', 'password', step);
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
|
||||||
document.getElementById('password-match-error').style.display = 'block';
|
|
||||||
document.getElementById('confirmPassword').classList.add('field-error');
|
|
||||||
showError('Passwords do not match', 'confirmPassword', step);
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
document.getElementById('password-match-error').style.display = 'none';
|
|
||||||
document.getElementById('confirmPassword').classList.remove('field-error');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate other required fields
|
|
||||||
const requiredFields = form.querySelectorAll('[required]');
|
|
||||||
requiredFields.forEach(field => {
|
|
||||||
if (field.id !== 'email' && !field.value.trim()) {
|
|
||||||
isValid = false;
|
|
||||||
field.classList.add('field-error');
|
|
||||||
} else if (field.id !== 'email') {
|
|
||||||
field.classList.remove('field-error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (step === 2) {
|
|
||||||
const companyName = document.getElementById('company-name').value;
|
|
||||||
|
|
||||||
if (!companyName) {
|
if (!companyName) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
showError('Company Name is Required', 'company-name', step);
|
showError('Company / Client Name is Required', null, step);
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,13 +610,10 @@ function getFormId(step) {
|
||||||
function saveStepData(step) {
|
function saveStepData(step) {
|
||||||
switch(step) {
|
switch(step) {
|
||||||
case 1:
|
case 1:
|
||||||
formData.userDetails.firstName = document.getElementById('first-name').value;
|
formData.userDetails.name = document.getElementById('company-name').value;
|
||||||
formData.userDetails.lastName = document.getElementById('last-name').value;
|
|
||||||
formData.userDetails.email = document.getElementById('email').value;
|
formData.userDetails.email = document.getElementById('email').value;
|
||||||
formData.userDetails.password = document.getElementById('password').value;
|
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
formData.userDetails.name = document.getElementById('company-name').value;
|
|
||||||
formData.userDetails.address = document.getElementById('address').value;
|
formData.userDetails.address = document.getElementById('address').value;
|
||||||
formData.userDetails.city = document.getElementById('city').value;
|
formData.userDetails.city = document.getElementById('city').value;
|
||||||
formData.userDetails.state = document.getElementById('state').value;
|
formData.userDetails.state = document.getElementById('state').value;
|
||||||
|
|
@ -735,50 +690,17 @@ async function submitForm() {
|
||||||
saveStepData(currentStep);
|
saveStepData(currentStep);
|
||||||
|
|
||||||
// Save payment details based on selected method
|
// Save payment details based on selected method
|
||||||
let cardNumber = document.getElementById('card-number');
|
if (formData.userDetails.paymentMethod === 'card') {
|
||||||
let expiryDate = document.getElementById('expiry-date');
|
formData.userDetails.cardNumber = document.getElementById('card-number').value;
|
||||||
let securityCode = document.getElementById('security-code');
|
formData.userDetails.expiryDate = document.getElementById('expiry-date').value;
|
||||||
let cardZip = document.getElementById('card-zip');
|
formData.userDetails.securityCode = document.getElementById('security-code').value;
|
||||||
let bankName = document.getElementById('bank-name');
|
formData.userDetails.cardZip = document.getElementById('card-zip').value;
|
||||||
let accountNumber = document.getElementById('account-number');
|
} else if (formData.userDetails.paymentMethod === 'bank') {
|
||||||
let routingNumber = document.getElementById('routing-number');
|
formData.userDetails.bankName = document.getElementById('bank-name').value;
|
||||||
let accountType = document.getElementById('account-type');
|
formData.userDetails.accountNumber = document.getElementById('account-number').value;
|
||||||
let accountHolderName = document.getElementById('account-holder-name');
|
formData.userDetails.routingNumber = document.getElementById('routing-number').value;
|
||||||
|
formData.userDetails.accountType = document.getElementById('account-type').value;
|
||||||
if(cardNumber.value) {
|
formData.userDetails.accountHolderName = document.getElementById('account-holder-name').value;
|
||||||
formData.userDetails.cardNumber = cardNumber.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(expiryDate.value) {
|
|
||||||
formData.userDetails.expiryDate = expiryDate.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(securityCode.value) {
|
|
||||||
formData.userDetails.securityCode = securityCode.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cardZip.value) {
|
|
||||||
formData.userDetails.cardZip = cardZip.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(bankName.value) {
|
|
||||||
formData.userDetails.bankName = bankName.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(accountNumber.value) {
|
|
||||||
formData.userDetails.accountNumber = accountNumber.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(routingNumber.value) {
|
|
||||||
formData.userDetails.routingNumber = routingNumber.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(accountType.value) {
|
|
||||||
formData.userDetails.accountType = accountType.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(accountHolderName.value) {
|
|
||||||
formData.userDetails.accountHolderName = accountHolderName.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
|
|
@ -812,7 +734,7 @@ async function submitForm() {
|
||||||
nextStep(6);
|
nextStep(6);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
// Handle server errors
|
// Handle server errors
|
||||||
|
|
@ -902,29 +824,28 @@ async function detectLocation() {
|
||||||
await populateStates(countryCode, 'state');
|
await populateStates(countryCode, 'state');
|
||||||
await populateStates(countryCode, 'billing-state');
|
await populateStates(countryCode, 'billing-state');
|
||||||
|
|
||||||
// auto populate state if region matches
|
const stateSelect = document.getElementById('state');
|
||||||
// const stateSelect = document.getElementById('state');
|
const billingStateSelect = document.getElementById('billing-state');
|
||||||
// const billingStateSelect = document.getElementById('billing-state');
|
|
||||||
|
|
||||||
// if (stateSelect && region) {
|
if (stateSelect && region) {
|
||||||
// const matchedState = Array.from(stateSelect.options).find(
|
const matchedState = Array.from(stateSelect.options).find(
|
||||||
// opt => opt.text.toLowerCase() === region.toLowerCase()
|
opt => opt.text.toLowerCase() === region.toLowerCase()
|
||||||
// );
|
);
|
||||||
// if (matchedState) {
|
if (matchedState) {
|
||||||
// stateSelect.value = matchedState.value;
|
stateSelect.value = matchedState.value;
|
||||||
// stateSelect.dispatchEvent(new Event('change'));
|
stateSelect.dispatchEvent(new Event('change'));
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (billingStateSelect && region) {
|
if (billingStateSelect && region) {
|
||||||
// const matchedBillingState = Array.from(billingStateSelect.options).find(
|
const matchedBillingState = Array.from(billingStateSelect.options).find(
|
||||||
// opt => opt.text.toLowerCase() === region.toLowerCase()
|
opt => opt.text.toLowerCase() === region.toLowerCase()
|
||||||
// );
|
);
|
||||||
// if (matchedBillingState) {
|
if (matchedBillingState) {
|
||||||
// billingStateSelect.value = matchedBillingState.value;
|
billingStateSelect.value = matchedBillingState.value;
|
||||||
// billingStateSelect.dispatchEvent(new Event('change'));
|
billingStateSelect.dispatchEvent(new Event('change'));
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error detecting location:', error);
|
console.error('Error detecting location:', error);
|
||||||
|
|
||||||
|
|
@ -1286,181 +1207,7 @@ function preventNumbers(event) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to validate first name
|
// Function to validate company/client name
|
||||||
function validateFirstName(input) {
|
|
||||||
const errorElement = document.getElementById('first-name-error');
|
|
||||||
const value = input.value;
|
|
||||||
|
|
||||||
// Check various invalid patterns
|
|
||||||
const validationResult = validateFirstNameContent(value);
|
|
||||||
|
|
||||||
if (!validationResult.isValid && value.trim().length > 0) {
|
|
||||||
errorElement.textContent = validationResult.message;
|
|
||||||
errorElement.style.display = 'block';
|
|
||||||
input.classList.add('field-error');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
errorElement.style.display = 'none';
|
|
||||||
input.classList.remove('field-error');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateFirstNameContent(firstName) {
|
|
||||||
const trimmed = firstName.trim();
|
|
||||||
|
|
||||||
if (trimmed.length === 0) {
|
|
||||||
return { isValid: false, message: 'First name is required' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check minimum length
|
|
||||||
if (trimmed.length < 2) {
|
|
||||||
return { isValid: false, message: 'First name must be at least 2 characters' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove whitespace for content analysis
|
|
||||||
const content = trimmed.replace(/\s/g, '');
|
|
||||||
|
|
||||||
// Check if contains only special characters
|
|
||||||
if (/^[^a-zA-Z0-9]+$/.test(content)) {
|
|
||||||
return { isValid: false, message: 'First name cannot be only special characters' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if contains at least some valid content (letters or numbers)
|
|
||||||
if (!/[a-zA-Z0-9]/.test(content)) {
|
|
||||||
return { isValid: false, message: 'First name must contain letters or numbers' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: Check for excessive special characters (more than 50%)
|
|
||||||
const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length;
|
|
||||||
if (specialCharCount > content.length / 2) {
|
|
||||||
return { isValid: false, message: 'First name contains too many special characters' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isValid: true, message: '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to prevent invalid character input in real-time
|
|
||||||
function preventInvalidFirstNameChars(event) {
|
|
||||||
const charCode = event.which ? event.which : event.keyCode;
|
|
||||||
const key = event.key;
|
|
||||||
|
|
||||||
// Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas
|
|
||||||
// Also allow common editing keys
|
|
||||||
if ((charCode >= 65 && charCode <= 90) || // A-Z
|
|
||||||
(charCode >= 97 && charCode <= 122) || // a-z
|
|
||||||
(charCode >= 48 && charCode <= 57) || // 0-9
|
|
||||||
charCode === 32 || // space
|
|
||||||
charCode === 45 || // hyphen
|
|
||||||
charCode === 39 || // apostrophe
|
|
||||||
charCode === 38 || // ampersand (&)
|
|
||||||
charCode === 46 || // period (.)
|
|
||||||
charCode === 44 || // comma (,)
|
|
||||||
charCode === 8 || // backspace
|
|
||||||
charCode === 9 || // tab
|
|
||||||
charCode === 37 || // left arrow
|
|
||||||
charCode === 39 || // right arrow
|
|
||||||
charCode === 16 || // shift
|
|
||||||
charCode === 17 || // ctrl
|
|
||||||
charCode === 18 || // alt
|
|
||||||
charCode === 91 || // cmd (Mac)
|
|
||||||
/^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent invalid characters
|
|
||||||
event.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to validate last name
|
|
||||||
function validateLastName(input) {
|
|
||||||
const errorElement = document.getElementById('last-name-error');
|
|
||||||
const value = input.value;
|
|
||||||
|
|
||||||
// Check various invalid patterns
|
|
||||||
const validationResult = validateLastNameContent(value);
|
|
||||||
|
|
||||||
if (!validationResult.isValid && value.trim().length > 0) {
|
|
||||||
errorElement.textContent = validationResult.message;
|
|
||||||
errorElement.style.display = 'block';
|
|
||||||
input.classList.add('field-error');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
errorElement.style.display = 'none';
|
|
||||||
input.classList.remove('field-error');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateLastNameContent(lastName) {
|
|
||||||
const trimmed = lastName.trim();
|
|
||||||
|
|
||||||
if (trimmed.length === 0) {
|
|
||||||
return { isValid: false, message: 'Last name is required' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check minimum length
|
|
||||||
if (trimmed.length < 2) {
|
|
||||||
return { isValid: false, message: 'Last name must be at least 2 characters' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove whitespace for content analysis
|
|
||||||
const content = trimmed.replace(/\s/g, '');
|
|
||||||
|
|
||||||
// Check if contains only special characters
|
|
||||||
if (/^[^a-zA-Z0-9]+$/.test(content)) {
|
|
||||||
return { isValid: false, message: 'Last name cannot be only special characters' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if contains at least some valid content (letters or numbers)
|
|
||||||
if (!/[a-zA-Z0-9]/.test(content)) {
|
|
||||||
return { isValid: false, message: 'Last name must contain letters or numbers' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: Check for excessive special characters (more than 50%)
|
|
||||||
const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length;
|
|
||||||
if (specialCharCount > content.length / 2) {
|
|
||||||
return { isValid: false, message: 'Last name contains too many special characters' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isValid: true, message: '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to prevent invalid character input in real-time
|
|
||||||
function preventInvalidLastNameChars(event) {
|
|
||||||
const charCode = event.which ? event.which : event.keyCode;
|
|
||||||
const key = event.key;
|
|
||||||
|
|
||||||
// Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas
|
|
||||||
// Also allow common editing keys
|
|
||||||
if ((charCode >= 65 && charCode <= 90) || // A-Z
|
|
||||||
(charCode >= 97 && charCode <= 122) || // a-z
|
|
||||||
(charCode >= 48 && charCode <= 57) || // 0-9
|
|
||||||
charCode === 32 || // space
|
|
||||||
charCode === 45 || // hyphen
|
|
||||||
charCode === 39 || // apostrophe
|
|
||||||
charCode === 38 || // ampersand (&)
|
|
||||||
charCode === 46 || // period (.)
|
|
||||||
charCode === 44 || // comma (,)
|
|
||||||
charCode === 8 || // backspace
|
|
||||||
charCode === 9 || // tab
|
|
||||||
charCode === 37 || // left arrow
|
|
||||||
charCode === 39 || // right arrow
|
|
||||||
charCode === 16 || // shift
|
|
||||||
charCode === 17 || // ctrl
|
|
||||||
charCode === 18 || // alt
|
|
||||||
charCode === 91 || // cmd (Mac)
|
|
||||||
/^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent invalid characters
|
|
||||||
event.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to validate company name
|
|
||||||
function validateCompanyName(input) {
|
function validateCompanyName(input) {
|
||||||
const errorElement = document.getElementById('company-name-error');
|
const errorElement = document.getElementById('company-name-error');
|
||||||
const value = input.value;
|
const value = input.value;
|
||||||
|
|
@ -1515,7 +1262,7 @@ function validateCompanyNameContent(companyName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to prevent invalid character input in real-time
|
// Function to prevent invalid character input in real-time
|
||||||
function preventInvalidCompanyNameChars(event) {
|
function preventInvalidCompanyChars(event) {
|
||||||
const charCode = event.which ? event.which : event.keyCode;
|
const charCode = event.which ? event.which : event.keyCode;
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
|
|
||||||
|
|
@ -2174,52 +1921,4 @@ function preventInvalidAccountHolderChars(event) {
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,117 +9,69 @@
|
||||||
|
|
||||||
// Countries and states data
|
// Countries and states data
|
||||||
const countries = [
|
const countries = [
|
||||||
{ code: 'US', name: 'United States' },
|
{ code: 'US', name: 'United States' },
|
||||||
{ code: 'CA', name: 'Canada' },
|
{ code: 'CA', name: 'Canada' },
|
||||||
{ code: 'GB', name: 'United Kingdom' },
|
{ code: 'GB', name: 'United Kingdom' },
|
||||||
{ code: 'AU', name: 'Australia' },
|
{ code: 'AU', name: 'Australia' },
|
||||||
{ code: 'IN', name: 'India' },
|
{ code: 'IN', name: 'India' },
|
||||||
{ code: 'DE', name: 'Germany' },
|
{ code: 'DE', name: 'Germany' },
|
||||||
{ code: 'FR', name: 'France' },
|
{ code: 'FR', name: 'France' },
|
||||||
{ code: 'BR', name: 'Brazil' },
|
{ code: 'BR', name: 'Brazil' },
|
||||||
{ code: 'JP', name: 'Japan' },
|
{ code: 'JP', name: 'Japan' },
|
||||||
{ code: 'CN', name: 'China' },
|
{ code: 'CN', name: 'China' }
|
||||||
{ code: 'MX', name: 'Mexico' },
|
|
||||||
{ code: 'ES', name: 'Spain' },
|
|
||||||
{ code: 'IT', name: 'Italy' },
|
|
||||||
{ code: 'NL', name: 'Netherlands' },
|
|
||||||
{ code: 'SE', name: 'Sweden' },
|
|
||||||
{ code: 'NO', name: 'Norway' },
|
|
||||||
{ code: 'DK', name: 'Denmark' },
|
|
||||||
{ code: 'FI', name: 'Finland' },
|
|
||||||
{ code: 'BE', name: 'Belgium' },
|
|
||||||
{ code: 'CH', name: 'Switzerland' },
|
|
||||||
{ code: 'AT', name: 'Austria' },
|
|
||||||
{ code: 'PT', name: 'Portugal' },
|
|
||||||
{ code: 'IE', name: 'Ireland' },
|
|
||||||
{ code: 'PL', name: 'Poland' },
|
|
||||||
{ code: 'CZ', name: 'Czech Republic' },
|
|
||||||
{ code: 'HU', name: 'Hungary' },
|
|
||||||
{ code: 'RO', name: 'Romania' },
|
|
||||||
{ code: 'GR', name: 'Greece' },
|
|
||||||
{ code: 'TR', name: 'Turkey' },
|
|
||||||
{ code: 'SA', name: 'Saudi Arabia' },
|
|
||||||
{ code: 'AE', name: 'United Arab Emirates' },
|
|
||||||
{ code: 'ZA', name: 'South Africa' },
|
|
||||||
{ code: 'NG', name: 'Nigeria' },
|
|
||||||
{ code: 'EG', name: 'Egypt' },
|
|
||||||
{ code: 'KE', name: 'Kenya' },
|
|
||||||
{ code: 'AR', name: 'Argentina' },
|
|
||||||
{ code: 'CO', name: 'Colombia' },
|
|
||||||
{ code: 'PE', name: 'Peru' },
|
|
||||||
{ code: 'CL', name: 'Chile' },
|
|
||||||
{ code: 'VE', name: 'Venezuela' },
|
|
||||||
{ code: 'KR', name: 'South Korea' },
|
|
||||||
{ code: 'SG', name: 'Singapore' },
|
|
||||||
{ code: 'MY', name: 'Malaysia' },
|
|
||||||
{ code: 'TH', name: 'Thailand' },
|
|
||||||
{ code: 'ID', name: 'Indonesia' },
|
|
||||||
{ code: 'PH', name: 'Philippines' },
|
|
||||||
{ code: 'VN', name: 'Vietnam' },
|
|
||||||
{ code: 'PK', name: 'Pakistan' },
|
|
||||||
{ code: 'BD', name: 'Bangladesh' },
|
|
||||||
{ code: 'LK', name: 'Sri Lanka' },
|
|
||||||
{ code: 'NP', name: 'Nepal' },
|
|
||||||
{ code: 'RU', name: 'Russia' },
|
|
||||||
{ code: 'UA', name: 'Ukraine' },
|
|
||||||
{ code: 'IL', name: 'Israel' },
|
|
||||||
{ code: 'NZ', name: 'New Zealand' },
|
|
||||||
{ code: 'HK', name: 'Hong Kong' },
|
|
||||||
{ code: 'TW', name: 'Taiwan' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// const states = {
|
const states = {
|
||||||
// 'US': [
|
'US': [
|
||||||
// { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
|
{ 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: '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: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
|
||||||
// { code: 'MI', name: 'Michigan' }
|
{ code: 'MI', name: 'Michigan' }
|
||||||
// ],
|
],
|
||||||
// 'CA': [
|
'CA': [
|
||||||
// { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
|
{ 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' }
|
{ code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
|
||||||
// ],
|
],
|
||||||
// 'GB': [
|
'GB': [
|
||||||
// { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
|
{ code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
|
||||||
// { code: 'NIR', name: 'Northern Ireland' }
|
{ code: 'NIR', name: 'Northern Ireland' }
|
||||||
// ],
|
],
|
||||||
// 'AU': [
|
'AU': [
|
||||||
// { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
|
{ 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' }
|
{ code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
|
||||||
// ],
|
],
|
||||||
// 'IN': [
|
'IN': [
|
||||||
// { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
|
{ 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' }
|
{ code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
|
||||||
// ],
|
],
|
||||||
// 'DE': [
|
'DE': [
|
||||||
// { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
|
{ 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' }
|
{ code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
|
||||||
// ],
|
],
|
||||||
// 'FR': [
|
'FR': [
|
||||||
// { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
|
{ 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: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
|
||||||
// { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
|
{ code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
|
||||||
// ],
|
],
|
||||||
// 'BR': [
|
'BR': [
|
||||||
// { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
|
{ 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á' }
|
{ code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
|
||||||
// ],
|
],
|
||||||
// 'JP': [
|
'JP': [
|
||||||
// { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
|
{ code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
|
||||||
// { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
|
{ code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
|
||||||
// ],
|
],
|
||||||
// 'CN': [
|
'CN': [
|
||||||
// { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
|
{ code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
|
||||||
// { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
|
{ code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
|
||||||
// ]
|
]
|
||||||
// };
|
};
|
||||||
|
|
||||||
// Initialize the form
|
// Initialize the form
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Populate country dropdown
|
// Populate country dropdown
|
||||||
const countrySelect = document.getElementById('country');
|
const countrySelect = document.getElementById('country');
|
||||||
countrySelect.innerHTML = '<option value="" disabled selected></option>';
|
countrySelect.innerHTML = '<option value=""></option>';
|
||||||
|
|
||||||
countries.forEach(country => {
|
countries.forEach(country => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
|
|
@ -128,34 +80,32 @@
|
||||||
countrySelect.appendChild(option);
|
countrySelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
autoDetectCountry();
|
|
||||||
|
|
||||||
// Add event listener for country change
|
// Add event listener for country change
|
||||||
// countrySelect.addEventListener('change', function() {
|
countrySelect.addEventListener('change', function() {
|
||||||
// const countryCode = this.value;
|
const countryCode = this.value;
|
||||||
// const stateSelect = document.getElementById('state');
|
const stateSelect = document.getElementById('state');
|
||||||
|
|
||||||
// // Show loading
|
// Show loading
|
||||||
// document.getElementById('state-loading').style.display = 'block';
|
document.getElementById('state-loading').style.display = 'block';
|
||||||
|
|
||||||
// // Clear current states
|
// Clear current states
|
||||||
// stateSelect.innerHTML = '<option value="" disabled selected></option>';
|
stateSelect.innerHTML = '<option value=""></option>';
|
||||||
|
|
||||||
// // Simulate API delay
|
// Simulate API delay
|
||||||
// setTimeout(() => {
|
setTimeout(() => {
|
||||||
// if (countryCode && states[countryCode]) {
|
if (countryCode && states[countryCode]) {
|
||||||
// states[countryCode].forEach(state => {
|
states[countryCode].forEach(state => {
|
||||||
// const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
// option.value = state.code;
|
option.value = state.code;
|
||||||
// option.textContent = state.name;
|
option.textContent = state.name;
|
||||||
// stateSelect.appendChild(option);
|
stateSelect.appendChild(option);
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Hide loading
|
// Hide loading
|
||||||
// document.getElementById('state-loading').style.display = 'none';
|
document.getElementById('state-loading').style.display = 'none';
|
||||||
// }, 500);
|
}, 500);
|
||||||
// });
|
});
|
||||||
|
|
||||||
|
|
||||||
// Add input event listeners to handle label animation
|
// Add input event listeners to handle label animation
|
||||||
|
|
@ -259,102 +209,6 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function autoDetectCountry() {
|
|
||||||
const countrySelect = document.getElementById('country');
|
|
||||||
// const loadingElement = document.getElementById('country-loading');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// loadingElement.style.display = 'block';
|
|
||||||
|
|
||||||
// Using ipapi.co API (free tier available)
|
|
||||||
const response = await fetch('https://ipapi.co/json/');
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Geolocation service unavailable');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data && data.country_code) {
|
|
||||||
const detectedCountryCode = data.country_code;
|
|
||||||
const detectedCountryName = data.country_name;
|
|
||||||
|
|
||||||
// Find if the detected country is in our list
|
|
||||||
const countryExists = countries.find(country =>
|
|
||||||
country.code === detectedCountryCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (countryExists) {
|
|
||||||
// Set the country in dropdown
|
|
||||||
countrySelect.value = detectedCountryCode;
|
|
||||||
|
|
||||||
// Trigger the change event to update UI
|
|
||||||
countrySelect.dispatchEvent(new Event('change'));
|
|
||||||
|
|
||||||
// Show success message
|
|
||||||
console.log(`Auto-detected country: ${detectedCountryName}`);
|
|
||||||
} else {
|
|
||||||
console.log('Detected country not in supported list');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Could not auto-detect country:', error.message);
|
|
||||||
// Fallback: Use browser's geolocation API as backup
|
|
||||||
fallbackGeolocation();
|
|
||||||
} finally {
|
|
||||||
// loadingElement.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback geolocation using browser's navigator.geolocation
|
|
||||||
function fallbackGeolocation() {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
console.log('Geolocation is not supported by this browser');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
async function(position) {
|
|
||||||
try {
|
|
||||||
const { latitude, longitude } = position.coords;
|
|
||||||
|
|
||||||
// Use OpenStreetMap Nominatim API for reverse geocoding
|
|
||||||
const response = await fetch(
|
|
||||||
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data && data.address && data.address.country_code) {
|
|
||||||
const countryCode = data.address.country_code.toUpperCase();
|
|
||||||
const countrySelect = document.getElementById('country');
|
|
||||||
|
|
||||||
const countryExists = countries.find(country =>
|
|
||||||
country.code === countryCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (countryExists) {
|
|
||||||
countrySelect.value = countryCode;
|
|
||||||
countrySelect.dispatchEvent(new Event('change'));
|
|
||||||
console.log(`Fallback geolocation detected: ${data.address.country}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Fallback geolocation failed:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(error) {
|
|
||||||
console.log('Browser geolocation error:', error.message);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enableHighAccuracy: false,
|
|
||||||
timeout: 10000,
|
|
||||||
maximumAge: 600000 // 10 minutes
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to navigate to any step (for tab navigation)
|
// Function to navigate to any step (for tab navigation)
|
||||||
function goToStep(step) {
|
function goToStep(step) {
|
||||||
// Only allow navigation to completed steps or the next step
|
// Only allow navigation to completed steps or the next step
|
||||||
|
|
@ -492,14 +346,6 @@
|
||||||
const email = document.getElementById('email').value;
|
const email = document.getElementById('email').value;
|
||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
if(!email && !password) {
|
|
||||||
showError('', 'email', step);
|
|
||||||
showError('', 'password', step);
|
|
||||||
showError('Please enter a valid email address and password', 'confirmPassword', step);
|
|
||||||
isValid = false;
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (!validateEmail(email)) {
|
if (!validateEmail(email)) {
|
||||||
|
|
@ -547,7 +393,7 @@
|
||||||
// Age validation
|
// Age validation
|
||||||
const age = document.getElementById('age').value;
|
const age = document.getElementById('age').value;
|
||||||
if (age && (age < 18 || age > 100)) {
|
if (age && (age < 18 || age > 100)) {
|
||||||
showError('You age must be 18 years or older to register', 'age', step);
|
showError('You age must be 18 years or older to register', 'dob', step);
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -615,35 +461,20 @@
|
||||||
showError('Please select a payment method', null, step);
|
showError('Please select a payment method', null, step);
|
||||||
isValid = false;
|
isValid = false;
|
||||||
} else {
|
} else {
|
||||||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||||
if (paymentMethod === 'payoneer') {
|
if (paymentMethod === 'payoneer') {
|
||||||
const payoneerAccount = document.getElementById('payoneerAccount').value;
|
const payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||||
if (!payoneerAccount) {
|
if (!payoneerAccount) {
|
||||||
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
||||||
isValid = false;
|
isValid = false;
|
||||||
} else if (!validateEmail(payoneerAccount)) {
|
}
|
||||||
showError('Please enter a valid email address for Payoneer', 'payoneerAccount', step);
|
} else if (paymentMethod === 'veem') {
|
||||||
isValid = false;
|
const veemAccount = document.getElementById('veemAccount').value;
|
||||||
}
|
if (!veemAccount) {
|
||||||
} else if (paymentMethod === 'veem') {
|
showError('Please enter your Veem account number', 'veemAccount', step);
|
||||||
const veemAccount = document.getElementById('veemAccount').value;
|
isValid = false;
|
||||||
if (!veemAccount) {
|
}
|
||||||
showError('Please enter your Veem account number', 'veemAccount', step);
|
}
|
||||||
isValid = false;
|
|
||||||
} else if (!validateEmail(veemAccount)) {
|
|
||||||
showError('Please enter a valid email address for Veem', 'veemAccount', step);
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
} else if (paymentMethod === 'wise') {
|
|
||||||
const wiseEmail = document.getElementById('wiseEmail').value;
|
|
||||||
if (!wiseEmail) {
|
|
||||||
showError('Please enter your Wise email address', 'wiseEmail', step);
|
|
||||||
isValid = false;
|
|
||||||
} else if (!validateEmail(wiseEmail)) {
|
|
||||||
showError('Please enter a valid email address for Wise', 'wiseEmail', step);
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -697,11 +528,10 @@
|
||||||
formData.userDetails.city = document.getElementById('city').value;
|
formData.userDetails.city = document.getElementById('city').value;
|
||||||
formData.userDetails.state = document.getElementById('state').value;
|
formData.userDetails.state = document.getElementById('state').value;
|
||||||
formData.userDetails.country = document.getElementById('country').value;
|
formData.userDetails.country = document.getElementById('country').value;
|
||||||
formData.status = document.getElementById('status').value;
|
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
formData.userDetails.phone = document.getElementById('phone').value;
|
formData.userDetails.phone = document.getElementById('phone').value;
|
||||||
// formData.userDetails.skype = document.getElementById('skype').value;
|
formData.userDetails.skype = document.getElementById('skype').value;
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
const selectedPayment = document.querySelector('.payment-option.selected');
|
const selectedPayment = document.querySelector('.payment-option.selected');
|
||||||
|
|
@ -709,15 +539,11 @@
|
||||||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||||
formData.userDetails.paymentMethod = paymentMethod;
|
formData.userDetails.paymentMethod = paymentMethod;
|
||||||
|
|
||||||
if (document.getElementById('payoneerAccount').value) {
|
if (paymentMethod === 'payoneer') {
|
||||||
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
|
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||||
}
|
} else if (paymentMethod === 'veem') {
|
||||||
if (document.getElementById('veemAccount').value) {
|
|
||||||
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
|
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
|
||||||
}
|
}
|
||||||
if (paymentMethod === 'wise') {
|
|
||||||
formData.userDetails.wiseEmail = document.getElementById('wiseEmail').value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -743,74 +569,9 @@
|
||||||
document.getElementById('payoneer-details').style.display = 'block';
|
document.getElementById('payoneer-details').style.display = 'block';
|
||||||
} else if (method === 'veem') {
|
} else if (method === 'veem') {
|
||||||
document.getElementById('veem-details').style.display = 'block';
|
document.getElementById('veem-details').style.display = 'block';
|
||||||
} else if (method === 'wise') {
|
|
||||||
document.getElementById('wise-details').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateWiseEmail(input) {
|
|
||||||
const wiseError = document.getElementById('wise-email-error');
|
|
||||||
const value = input.value.trim();
|
|
||||||
|
|
||||||
// Remove any validation styling first
|
|
||||||
input.style.border = '';
|
|
||||||
wiseError.style.display = 'none';
|
|
||||||
|
|
||||||
if (value.length > 0) {
|
|
||||||
// Check if it's a valid email
|
|
||||||
if (!validateEmail(value)) {
|
|
||||||
wiseError.textContent = 'Please enter a valid email address for Wise';
|
|
||||||
wiseError.style.display = 'block';
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateVeemEmail(input) {
|
|
||||||
const wiseError = document.getElementById('veem-error');
|
|
||||||
const value = input.value.trim();
|
|
||||||
|
|
||||||
// Remove any validation styling first
|
|
||||||
input.style.border = '';
|
|
||||||
wiseError.style.display = 'none';
|
|
||||||
|
|
||||||
if (value.length > 0) {
|
|
||||||
// Check if it's a valid email
|
|
||||||
if (!validateEmail(value)) {
|
|
||||||
wiseError.textContent = 'Please enter a valid email address for Veem';
|
|
||||||
wiseError.style.display = 'block';
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validatePayoneerEmail(input) {
|
|
||||||
const wiseError = document.getElementById('payoneer-error');
|
|
||||||
const value = input.value.trim();
|
|
||||||
|
|
||||||
// Remove any validation styling first
|
|
||||||
input.style.border = '';
|
|
||||||
wiseError.style.display = 'none';
|
|
||||||
|
|
||||||
if (value.length > 0) {
|
|
||||||
// Check if it's a valid email
|
|
||||||
if (!validateEmail(value)) {
|
|
||||||
wiseError.textContent = 'Please enter a valid email address for Payoneer';
|
|
||||||
wiseError.style.display = 'block';
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate a strong password
|
// Function to generate a strong password
|
||||||
function generatePassword() {
|
function generatePassword() {
|
||||||
const length = 12;
|
const length = 12;
|
||||||
|
|
@ -981,13 +742,11 @@
|
||||||
// Validate age
|
// Validate age
|
||||||
if (age < 18) {
|
if (age < 18) {
|
||||||
// alert('You must be 18 years or older to register');
|
// alert('You must be 18 years or older to register');
|
||||||
// showError('You Age must be 18 years or older to register', 'dob');
|
showError('You Age must be 18 years or older to register', 'dob',);
|
||||||
showError('You Age must be 18 years or older to register', 'age');
|
|
||||||
ageInput.style.border = '1px solid #ef4444';
|
ageInput.style.border = '1px solid #ef4444';
|
||||||
} else if (age > 100) {
|
} else if (age > 100) {
|
||||||
// alert('Please enter a valid age');
|
// alert('Please enter a valid age');
|
||||||
// showError('Please enter a valid age', 'dob');
|
showError('Please enter a valid age', 'dob');
|
||||||
showError('Please enter a valid age', 'age');
|
|
||||||
ageInput.style.border = '1px solid #ef4444';
|
ageInput.style.border = '1px solid #ef4444';
|
||||||
} else {
|
} else {
|
||||||
ageInput.style.border = '';
|
ageInput.style.border = '';
|
||||||
|
|
@ -1052,37 +811,18 @@
|
||||||
const payoneerError = document.getElementById('payoneer-error');
|
const payoneerError = document.getElementById('payoneer-error');
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
|
|
||||||
// Remove any validation styling first
|
// Payoneer typically uses email format or alphanumeric IDs
|
||||||
input.style.border = '';
|
// Adjust validation rules based on Payoneer's actual format requirements
|
||||||
payoneerError.style.display = 'none';
|
if (value.length > 0 && (value.length < 5 || value.length > 20)) {
|
||||||
|
payoneerError.textContent = 'Payoneer account must be 5-20 characters';
|
||||||
if (value.length > 0) {
|
payoneerError.style.display = 'block';
|
||||||
// Check length
|
input.style.border = '1px solid #ef4445';
|
||||||
if (value.length < 5 || value.length > 20) {
|
return false;
|
||||||
payoneerError.textContent = 'Payoneer account must be 5-20 digits';
|
} else {
|
||||||
payoneerError.style.display = 'block';
|
payoneerError.style.display = 'none';
|
||||||
input.style.border = '1px solid #ef4444';
|
input.style.border = '';
|
||||||
return false;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
// Check if contains only digits
|
|
||||||
if (!/^\d+$/.test(value)) {
|
|
||||||
payoneerError.textContent = 'Payoneer account must contain digits only';
|
|
||||||
payoneerError.style.display = 'block';
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all zeros
|
|
||||||
if (/^0+$/.test(value)) {
|
|
||||||
payoneerError.textContent = 'Payoneer account cannot be all zeros';
|
|
||||||
payoneerError.style.display = 'block';
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to validate Veem account number
|
// Function to validate Veem account number
|
||||||
|
|
@ -1090,37 +830,17 @@
|
||||||
const veemError = document.getElementById('veem-error');
|
const veemError = document.getElementById('veem-error');
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
|
|
||||||
// Remove any validation styling first
|
// Veem account numbers are typically longer alphanumeric strings
|
||||||
input.style.border = '';
|
if (value.length > 0 && (value.length < 8 || value.length > 25)) {
|
||||||
veemError.style.display = 'none';
|
veemError.textContent = 'Veem account must be 8-25 characters';
|
||||||
|
veemError.style.display = 'block';
|
||||||
if (value.length > 0) {
|
input.style.border = '1px solid #ef4445';
|
||||||
// Check length
|
return false;
|
||||||
if (value.length < 8 || value.length > 25) {
|
} else {
|
||||||
veemError.textContent = 'Veem account must be 8-25 digits';
|
veemError.style.display = 'none';
|
||||||
veemError.style.display = 'block';
|
input.style.border = '';
|
||||||
input.style.border = '1px solid #ef4444';
|
return true;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if contains only digits
|
|
||||||
if (!/^\d+$/.test(value)) {
|
|
||||||
veemError.textContent = 'Veem account must contain digits only';
|
|
||||||
veemError.style.display = 'block';
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all zeros
|
|
||||||
if (/^0+$/.test(value)) {
|
|
||||||
veemError.textContent = 'Veem account cannot be all zeros';
|
|
||||||
veemError.style.display = 'block';
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(message, fieldId = null, step = null) {
|
function showError(message, fieldId = null, step = null) {
|
||||||
|
|
@ -1181,28 +901,4 @@
|
||||||
|
|
||||||
function getCurrentStep() {
|
function getCurrentStep() {
|
||||||
return currentStep;
|
return currentStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeSelect(selectElement) {
|
|
||||||
const selectedValue = selectElement.value;
|
|
||||||
let option = selectElement.querySelector('option:disabled');
|
|
||||||
|
|
||||||
if (selectedValue !== "") {
|
|
||||||
option.style.display = "none";
|
|
||||||
} else {
|
|
||||||
option.style.display = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateSkypeId(input) {
|
|
||||||
const value = input.value.trim();
|
|
||||||
|
|
||||||
if (value && !/^[a-zA-Z][a-zA-Z0-9\.,\-_]+$/.test(value)) {
|
|
||||||
input.style.border = '1px solid #ef4444';
|
|
||||||
showError('Invalid Skype ID', 'skype', 3);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
input.style.border = '';
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js";
|
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
|
||||||
|
|
||||||
// Getting client dashboard data - all VAs, projects, and active VAs
|
// Getting client dashboard data - all VAs, projects, and active VAs
|
||||||
async function client_dashboard(req, res) {
|
async function client_dashboard(req, res) {
|
||||||
|
|
@ -69,7 +68,12 @@ async function client_dashboard(req, res) {
|
||||||
$cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0]
|
$cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
lastActivity: { $first: "$time" }
|
lastActivity: { $first: "$time" },
|
||||||
|
projectsWorked: {
|
||||||
|
$addToSet: "$projectId"
|
||||||
|
},
|
||||||
|
// Get all login logs for this VA
|
||||||
|
allLogs: { $push: "$$ROOT" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -81,12 +85,22 @@ async function client_dashboard(req, res) {
|
||||||
|
|
||||||
const activeVaIds = agg.map((a) => a._id).filter(Boolean);
|
const activeVaIds = agg.map((a) => a._id).filter(Boolean);
|
||||||
|
|
||||||
|
// Get active VA details
|
||||||
|
const activeVAs = await VAModel.find({
|
||||||
|
_id: { $in: activeVaIds },
|
||||||
|
clientIds: clientID
|
||||||
|
})
|
||||||
|
.select('userDetails.firstName userDetails.lastName userDetails.email userDetails.phone status createdAt projectIds')
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
// Create mapping for last log and activity data
|
// Create mapping for last log and activity data
|
||||||
const activityByVa = agg.reduce((acc, a) => {
|
const activityByVa = agg.reduce((acc, a) => {
|
||||||
acc[String(a._id)] = {
|
acc[String(a._id)] = {
|
||||||
lastLog: a.lastLog,
|
lastLog: a.lastLog,
|
||||||
totalLogins: a.totalLogins,
|
totalLogins: a.totalLogins,
|
||||||
lastActivity: a.lastActivity
|
lastActivity: a.lastActivity,
|
||||||
|
projectsWorked: a.projectsWorked || []
|
||||||
};
|
};
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
@ -112,14 +126,14 @@ async function client_dashboard(req, res) {
|
||||||
logsByVa[vaId].push(log);
|
logsByVa[vaId].push(log);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate total worked time for each VA
|
// Calculate total worked time for each VA using the same logic as va_log_work_report_callback
|
||||||
for (const [vaId, logs] of Object.entries(logsByVa)) {
|
for (const [vaId, logs] of Object.entries(logsByVa)) {
|
||||||
let totalWorkSeconds = 0;
|
let totalWorkSeconds = 0;
|
||||||
|
|
||||||
// Group logs by date (YYYY-MM-DD format)
|
// Group logs by date (YYYY-MM-DD format)
|
||||||
const logsByDate = {};
|
const logsByDate = {};
|
||||||
logs.forEach(log => {
|
logs.forEach(log => {
|
||||||
const date = new Date(log.time).toISOString().split('T')[0];
|
const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
if (!logsByDate[date]) {
|
if (!logsByDate[date]) {
|
||||||
logsByDate[date] = [];
|
logsByDate[date] = [];
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +147,7 @@ async function client_dashboard(req, res) {
|
||||||
|
|
||||||
let dayWorkSeconds = 0;
|
let dayWorkSeconds = 0;
|
||||||
let lastLogin = null;
|
let lastLogin = null;
|
||||||
|
let lastProjectId = null;
|
||||||
|
|
||||||
// If day has adjustment logs, only process adjustment logs
|
// If day has adjustment logs, only process adjustment logs
|
||||||
// If no adjustment logs, process all logs for that day
|
// If no adjustment logs, process all logs for that day
|
||||||
|
|
@ -149,10 +164,14 @@ async function client_dashboard(req, res) {
|
||||||
|
|
||||||
if (log.loggedType === "LOGIN") {
|
if (log.loggedType === "LOGIN") {
|
||||||
lastLogin = time;
|
lastLogin = time;
|
||||||
|
lastProjectId = log.projectId?.toString() || null;
|
||||||
} else if (log.loggedType === "LOGOUT" && lastLogin) {
|
} else if (log.loggedType === "LOGOUT" && lastLogin) {
|
||||||
const diffSec = (time - lastLogin) / 1000;
|
const diffSec = (time - lastLogin) / 1000;
|
||||||
dayWorkSeconds += diffSec;
|
dayWorkSeconds += diffSec;
|
||||||
|
|
||||||
|
// reset after each pair
|
||||||
lastLogin = null;
|
lastLogin = null;
|
||||||
|
lastProjectId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,93 +184,25 @@ async function client_dashboard(req, res) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: Calculate project-wise work time for each VA
|
|
||||||
const projectWorkTimeByVa = {};
|
|
||||||
|
|
||||||
for (const [vaId, logs] of Object.entries(logsByVa)) {
|
|
||||||
const projectWorkTime = {};
|
|
||||||
|
|
||||||
// Group logs by project
|
|
||||||
const logsByProject = {};
|
|
||||||
logs.forEach(log => {
|
|
||||||
const projectId = log.projectId?.toString();
|
|
||||||
if (projectId) {
|
|
||||||
if (!logsByProject[projectId]) {
|
|
||||||
logsByProject[projectId] = [];
|
|
||||||
}
|
|
||||||
logsByProject[projectId].push(log);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate work time per project
|
|
||||||
for (const [projectId, projectLogs] of Object.entries(logsByProject)) {
|
|
||||||
let projectWorkSeconds = 0;
|
|
||||||
|
|
||||||
// Group logs by date
|
|
||||||
const logsByDate = {};
|
|
||||||
projectLogs.forEach(log => {
|
|
||||||
const date = new Date(log.time).toISOString().split('T')[0];
|
|
||||||
if (!logsByDate[date]) {
|
|
||||||
logsByDate[date] = [];
|
|
||||||
}
|
|
||||||
logsByDate[date].push(log);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process each day separately
|
|
||||||
for (const [date, dayLogs] of Object.entries(logsByDate)) {
|
|
||||||
const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
|
|
||||||
let dayWorkSeconds = 0;
|
|
||||||
let lastLogin = null;
|
|
||||||
|
|
||||||
const logsToProcess = hasAdjustmentLogs
|
|
||||||
? dayLogs.filter(log => log.isAdjustment === true)
|
|
||||||
: dayLogs;
|
|
||||||
|
|
||||||
logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
||||||
|
|
||||||
for (const log of logsToProcess) {
|
|
||||||
const time = new Date(log.time);
|
|
||||||
|
|
||||||
if (log.loggedType === "LOGIN") {
|
|
||||||
lastLogin = time;
|
|
||||||
} else if (log.loggedType === "LOGOUT" && lastLogin) {
|
|
||||||
const diffSec = (time - lastLogin) / 1000;
|
|
||||||
dayWorkSeconds += diffSec;
|
|
||||||
lastLogin = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
projectWorkSeconds += dayWorkSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
projectWorkTime[projectId] = {
|
|
||||||
totalSeconds: projectWorkSeconds,
|
|
||||||
formatted: formatTime(projectWorkSeconds),
|
|
||||||
projectName: projectMap[projectId]?.name || 'Unknown Project'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
projectWorkTimeByVa[vaId] = projectWorkTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate session duration for currently active VAs
|
// Calculate session duration for currently active VAs
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
// Format all assigned VAs - Only include data needed for frontend
|
// Format all assigned VAs
|
||||||
const allVAsFormatted = allAssignedVAs.map((v) => {
|
const allVAsFormatted = allAssignedVAs.map((v) => {
|
||||||
const activity = activityByVa[String(v._id)] || {};
|
const activity = activityByVa[String(v._id)] || {};
|
||||||
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
|
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
|
||||||
const lastLog = activity.lastLog;
|
const lastLog = activity.lastLog;
|
||||||
const projectWorkTime = projectWorkTimeByVa[String(v._id)] || {};
|
|
||||||
|
|
||||||
// Calculate current session duration if still logged in
|
// Calculate current session duration if still logged in
|
||||||
let currentSessionDuration = null;
|
let currentSessionDuration = null;
|
||||||
let isCurrentlyActive = false;
|
let isCurrentlyActive = false;
|
||||||
|
let currentSessionFormatted = null;
|
||||||
let currentProjectDetails = null;
|
let currentProjectDetails = null;
|
||||||
|
|
||||||
if (lastLog && lastLog.loggedType === "LOGIN") {
|
if (lastLog && lastLog.loggedType === "LOGIN") {
|
||||||
const loginTime = new Date(lastLog.time);
|
const loginTime = new Date(lastLog.time);
|
||||||
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
|
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
|
||||||
|
currentSessionFormatted = formatTime(currentSessionDuration);
|
||||||
isCurrentlyActive = true;
|
isCurrentlyActive = true;
|
||||||
|
|
||||||
// Consider VA inactive if last login was more than 24 hours ago
|
// Consider VA inactive if last login was more than 24 hours ago
|
||||||
|
|
@ -272,29 +223,6 @@ async function client_dashboard(req, res) {
|
||||||
// Combine first and last name
|
// Combine first and last name
|
||||||
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
|
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
|
||||||
|
|
||||||
// Filter: Only include projects that belong to this client
|
|
||||||
const clientProjectsAssigned = (v.projectIds || []).filter(projectId =>
|
|
||||||
clientProjectIds.some(clientProjectId =>
|
|
||||||
clientProjectId.toString() === projectId.toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Format project work time for popup
|
|
||||||
const projectsWithWorkTime = clientProjectsAssigned.map(projectId => {
|
|
||||||
const projectIdStr = projectId.toString();
|
|
||||||
const workData = projectWorkTime[projectIdStr] || { totalSeconds: 0, formatted: '00:00:00' };
|
|
||||||
const project = projectMap[projectIdStr];
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: projectId,
|
|
||||||
name: project?.name || 'Unknown Project',
|
|
||||||
totalWorkedTime: workData.formatted,
|
|
||||||
totalWorkedSeconds: workData.totalSeconds,
|
|
||||||
isCurrentlyWorking: currentProjectDetails?.id?.toString() === projectIdStr,
|
|
||||||
hourlyRate: project?.hourlyRate || 0
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: v._id,
|
id: v._id,
|
||||||
name: fullName,
|
name: fullName,
|
||||||
|
|
@ -303,23 +231,102 @@ async function client_dashboard(req, res) {
|
||||||
status: v.status || 'active',
|
status: v.status || 'active',
|
||||||
isCurrentlyActive: isCurrentlyActive,
|
isCurrentlyActive: isCurrentlyActive,
|
||||||
lastLoginTime: lastLog?.time || null,
|
lastLoginTime: lastLog?.time || null,
|
||||||
|
lastActivity: activity.lastActivity,
|
||||||
currentSessionDuration: currentSessionDuration,
|
currentSessionDuration: currentSessionDuration,
|
||||||
|
currentSessionFormatted: currentSessionFormatted,
|
||||||
totalWorkedTime: workedTime.formatted,
|
totalWorkedTime: workedTime.formatted,
|
||||||
totalWorkedSeconds: workedTime.totalSeconds,
|
totalWorkedSeconds: workedTime.totalSeconds,
|
||||||
|
totalLogins: activity.totalLogins || 0,
|
||||||
lastProject: currentProjectDetails,
|
lastProject: currentProjectDetails,
|
||||||
lastProjectId: lastLog?.projectId || null,
|
lastProjectId: lastLog?.projectId || null,
|
||||||
projectsAssigned: clientProjectsAssigned.length,
|
projectsWorked: activity.projectsWorked || [],
|
||||||
// Detailed project information for popup
|
projectsAssigned: v.projectIds?.length || 0,
|
||||||
projects: projectsWithWorkTime,
|
|
||||||
vaSince: v.createdAt
|
vaSince: v.createdAt
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const dayNumber = getDayNumber();
|
// Format active VAs (currently working)
|
||||||
|
const activeVAsFormatted = activeVAs.map((v) => {
|
||||||
|
const activity = activityByVa[String(v._id)] || {};
|
||||||
|
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
|
||||||
|
const lastLog = activity.lastLog;
|
||||||
|
|
||||||
|
let currentSessionDuration = null;
|
||||||
|
let currentSessionFormatted = null;
|
||||||
|
let currentProjectDetails = null;
|
||||||
|
|
||||||
|
if (lastLog && lastLog.loggedType === "LOGIN") {
|
||||||
|
const loginTime = new Date(lastLog.time);
|
||||||
|
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
|
||||||
|
currentSessionFormatted = formatTime(currentSessionDuration);
|
||||||
|
|
||||||
|
// Get current project details
|
||||||
|
if (lastLog.projectId && projectMap[lastLog.projectId.toString()]) {
|
||||||
|
currentProjectDetails = {
|
||||||
|
id: lastLog.projectId,
|
||||||
|
name: projectMap[lastLog.projectId.toString()].name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine first and last name
|
||||||
|
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: v._id,
|
||||||
|
name: fullName,
|
||||||
|
email: v.userDetails?.email,
|
||||||
|
phone: v.userDetails?.phone,
|
||||||
|
status: v.status || 'active',
|
||||||
|
lastLoginTime: lastLog?.time || null,
|
||||||
|
currentSessionDuration: currentSessionDuration,
|
||||||
|
currentSessionFormatted: currentSessionFormatted,
|
||||||
|
totalWorkedTime: workedTime.formatted,
|
||||||
|
totalWorkedSeconds: workedTime.totalSeconds,
|
||||||
|
totalLogins: activity.totalLogins || 0,
|
||||||
|
currentProject: currentProjectDetails,
|
||||||
|
currentProjectId: lastLog?.projectId || null,
|
||||||
|
projectsAssigned: v.projectIds?.length || 0,
|
||||||
|
vaSince: v.createdAt
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format projects with additional data
|
||||||
|
const projectsFormatted = clientProjects.map((p) => {
|
||||||
|
// Find VAs currently working on this project
|
||||||
|
const activeVAsOnProject = allVAsFormatted.filter(va =>
|
||||||
|
va.isCurrentlyActive && va.lastProjectId &&
|
||||||
|
va.lastProjectId.toString() === p._id.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find all VAs assigned to this project
|
||||||
|
const assignedVAs = allAssignedVAs.filter(va =>
|
||||||
|
va.projectIds && va.projectIds.some(projectId =>
|
||||||
|
projectId.toString() === p._id.toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: p._id,
|
||||||
|
name: p.name,
|
||||||
|
hourlyRate: p.hourlyRate,
|
||||||
|
totalLoggedHours: p.totalLoggedHours || 0,
|
||||||
|
dedicatedHours: p.dedicatedHours || 0,
|
||||||
|
vaAssigned: assignedVAs.length,
|
||||||
|
vaCurrentlyWorking: activeVAsOnProject.length,
|
||||||
|
activeVAs: activeVAsOnProject.map(va => ({
|
||||||
|
id: va.id,
|
||||||
|
name: va.name,
|
||||||
|
currentSession: va.currentSessionFormatted,
|
||||||
|
lastLoginTime: va.lastLoginTime
|
||||||
|
})),
|
||||||
|
createdAt: p.createdAt
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate dashboard statistics
|
// Calculate dashboard statistics
|
||||||
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
|
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
|
||||||
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours[dayNumber] || 0), 0);
|
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0);
|
||||||
|
|
||||||
const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive);
|
const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive);
|
||||||
const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive);
|
const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive);
|
||||||
|
|
@ -327,22 +334,45 @@ async function client_dashboard(req, res) {
|
||||||
// Calculate total worked time across all VAs
|
// Calculate total worked time across all VAs
|
||||||
const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0);
|
const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0);
|
||||||
|
|
||||||
|
// Calculate projects with active workers
|
||||||
|
const projectsWithActiveWorkers = projectsFormatted.filter(p => p.vaCurrentlyWorking > 0).length;
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
// Only send data that frontend actually uses
|
// All assigned VAs with their status
|
||||||
allVAs: allVAsFormatted
|
allVAs: {
|
||||||
|
active: currentlyActiveVAs,
|
||||||
|
inactive: inactiveVAs,
|
||||||
|
total: allVAsFormatted
|
||||||
|
},
|
||||||
|
// Currently working VAs (active session)
|
||||||
|
activeVAs: activeVAsFormatted,
|
||||||
|
// All client projects
|
||||||
|
projects: projectsFormatted
|
||||||
},
|
},
|
||||||
summary: {
|
summary: {
|
||||||
totalVAs: allAssignedVAs.length,
|
totalVAs: allAssignedVAs.length,
|
||||||
activeVAs: currentlyActiveVAs.length,
|
activeVAs: currentlyActiveVAs.length,
|
||||||
inactiveVAs: inactiveVAs.length,
|
inactiveVAs: inactiveVAs.length,
|
||||||
totalProjects: clientProjects.length,
|
totalProjects: clientProjects.length,
|
||||||
|
projectsWithActiveWorkers: projectsWithActiveWorkers,
|
||||||
totalHoursLogged: totalHoursLogged,
|
totalHoursLogged: totalHoursLogged,
|
||||||
totalDedicatedHours: totalDedicatedHours,
|
totalDedicatedHours: totalDedicatedHours,
|
||||||
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
|
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
|
||||||
utilizationRate: totalDedicatedHours > 0 ?
|
utilizationRate: totalDedicatedHours > 0 ?
|
||||||
((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%'
|
((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%',
|
||||||
|
lastUpdated: currentTime.toISOString()
|
||||||
|
},
|
||||||
|
counts: {
|
||||||
|
vas: {
|
||||||
|
total: allAssignedVAs.length,
|
||||||
|
active: currentlyActiveVAs.length,
|
||||||
|
inactive: inactiveVAs.length
|
||||||
|
},
|
||||||
|
projects: clientProjects.length,
|
||||||
|
activeSessions: activeVAsFormatted.length,
|
||||||
|
projectsWithActivity: projectsWithActiveWorkers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -356,7 +386,7 @@ async function client_dashboard(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to format time
|
// Helper function to format time (same as in va_log_work_report_callback)
|
||||||
function formatTime(seconds) {
|
function formatTime(seconds) {
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
async function va_credits_get(req, res) {
|
|
||||||
try {
|
|
||||||
const clientId = req.body.clientId;
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/credit/${clientId}`;
|
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: result.data
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
salesforceError.isSalesforceError = true;
|
|
||||||
throw salesforceError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { va_credits_get };
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import { VAModel, ClientModel, ProjectModel, WorkTrackModel } from "../../../models/vaModel.js";
|
|
||||||
|
|
||||||
async function clientDashboardGetVaLogsController(req, res) {
|
|
||||||
try {
|
|
||||||
const clientId = req.body.clientId;
|
|
||||||
const clientUser = await ClientModel.findById(clientId);
|
|
||||||
if (!clientUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Client not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { vaId } = req.params;
|
|
||||||
const vaUser = await VAModel.findById(vaId);
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Virtual Assistant not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get shared active projects
|
|
||||||
const sharedProjects = await ProjectModel.find({
|
|
||||||
clientId: clientUser._id,
|
|
||||||
'vaAssigned.vaId': vaId,
|
|
||||||
status: "ACTIVE"
|
|
||||||
}).select('name');
|
|
||||||
|
|
||||||
if (!sharedProjects || sharedProjects.length === 0) {
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: "No shared projects found",
|
|
||||||
data: { projectLogs: [] }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectIds = sharedProjects.map(p => p._id);
|
|
||||||
|
|
||||||
// Get work tracks for the last 15 days
|
|
||||||
const fifteenDaysAgo = new Date();
|
|
||||||
fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15);
|
|
||||||
|
|
||||||
// Get all work tracks
|
|
||||||
const workTracks = await WorkTrackModel.find({
|
|
||||||
vaId: vaId,
|
|
||||||
projectId: { $in: projectIds },
|
|
||||||
from: { $gte: fifteenDaysAgo }
|
|
||||||
})
|
|
||||||
.sort({ from: -1 })
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
// Build project logs array
|
|
||||||
const projectLogs = [];
|
|
||||||
let totalWorkTracks = 0;
|
|
||||||
let totalHours = 0;
|
|
||||||
|
|
||||||
sharedProjects.forEach(project => {
|
|
||||||
// Filter tracks for this project
|
|
||||||
const projectTracks = workTracks.filter(track =>
|
|
||||||
track.projectId.toString() === project._id.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only add project if it has tracks
|
|
||||||
if (projectTracks.length > 0) {
|
|
||||||
// Calculate project total hours
|
|
||||||
const projectTotalHours = projectTracks.reduce((sum, track) => sum + (track.totalTime || 0), 0);
|
|
||||||
|
|
||||||
projectLogs.push({
|
|
||||||
projectDetails: {
|
|
||||||
id: project._id,
|
|
||||||
name: project.name,
|
|
||||||
hourlyRate: project.hourlyRate
|
|
||||||
},
|
|
||||||
workTrackLogs: projectTracks.map(track => ({
|
|
||||||
id: track._id,
|
|
||||||
from: track.from,
|
|
||||||
to: track.to,
|
|
||||||
totalTime: track.totalTime,
|
|
||||||
logs: track.logs || [],
|
|
||||||
createdAt: track.createdAt,
|
|
||||||
updatedAt: track.updatedAt
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
totalWorkTracks += projectTracks.length;
|
|
||||||
totalHours += projectTotalHours;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
success: true,
|
|
||||||
message: "VA logs retrieved successfully",
|
|
||||||
data: {
|
|
||||||
projectLogs: projectLogs
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add summary if needed (optional)
|
|
||||||
if (projectLogs.length > 0) {
|
|
||||||
response.data.summary = {
|
|
||||||
totalProjects: projectLogs.length,
|
|
||||||
totalWorkTracks: totalWorkTracks,
|
|
||||||
totalHours: totalHours,
|
|
||||||
dateRange: {
|
|
||||||
startDate: fifteenDaysAgo.toISOString().split('T')[0],
|
|
||||||
endDate: new Date().toISOString().split('T')[0]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json(response);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching VA logs:', error);
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Failed to load VA logs'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { clientDashboardGetVaLogsController };
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
|
import { ClientModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
|
||||||
|
|
||||||
function formatHoursToReadable(hours) {
|
function formatHoursToReadable(hours) {
|
||||||
const wholeHours = Math.floor(hours);
|
const wholeHours = Math.floor(hours);
|
||||||
|
|
@ -16,7 +15,7 @@ function formatHoursToReadable(hours) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getting client dashboard data - projects with assigned VA details
|
// Getting client dashboard data - all VAs and projects
|
||||||
async function client_dashboard_projects(req, res) {
|
async function client_dashboard_projects(req, res) {
|
||||||
try {
|
try {
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
@ -32,139 +31,64 @@ async function client_dashboard_projects(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientEmail = decoded.email;
|
const clientEmail = decoded.email;
|
||||||
|
|
||||||
// STEP 1: Get client ID from token
|
|
||||||
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
|
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
|
||||||
if (!clientUser) {
|
if (!clientUser) {
|
||||||
return res.status(404).json({ success: false, message: "Client not found" });
|
return res.status(404).json({ success: false, message: "Client not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientId = clientUser._id;
|
// Get all VA IDs and Project IDs assigned to this client
|
||||||
|
const clientVaIds = clientUser.vaIds || [];
|
||||||
|
const clientProjectIds = clientUser.projectIds || [];
|
||||||
|
|
||||||
// STEP 2: Find all projects of this client
|
// Get all VAs assigned to this client (with required fields only)
|
||||||
const clientProjects = await ProjectModel.find({
|
|
||||||
clientId: clientId
|
|
||||||
})
|
|
||||||
.select('name hourlyRate vaAssigned dedicatedHours status')
|
|
||||||
.lean()
|
|
||||||
.exec();
|
|
||||||
|
|
||||||
// Get all VA IDs from all projects for activity check
|
|
||||||
const allProjectVaIds = [];
|
|
||||||
clientProjects.forEach(project => {
|
|
||||||
if (project.vaAssigned && project.vaAssigned.length > 0) {
|
|
||||||
allProjectVaIds.push(...project.vaAssigned);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// STEP 3: Get all VAs assigned to these projects
|
|
||||||
const allAssignedVAs = await VAModel.find({
|
const allAssignedVAs = await VAModel.find({
|
||||||
_id: { $in: allProjectVaIds.vaId }
|
_id: { $in: clientVaIds }
|
||||||
})
|
})
|
||||||
.select('userDetails.firstName userDetails.lastName userDetails.email status')
|
.select('userDetails.firstName userDetails.lastName userDetails.email status')
|
||||||
.lean()
|
.lean()
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
// Create a map for quick VA lookup
|
// Get All Projects of client with required fields
|
||||||
const vaMap = allAssignedVAs.reduce((acc, va) => {
|
const clientProjects = await ProjectModel.find({
|
||||||
acc[va._id.toString()] = va;
|
_id: { $in: clientProjectIds }
|
||||||
return acc;
|
})
|
||||||
}, {});
|
.select('name hourlyRate vaAssigned dedicatedHours status')
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
// STEP 4: Check VA activity per project
|
// Format VAs with only required fields
|
||||||
const currentTime = new Date();
|
const vasFormatted = allAssignedVAs.map((va) => ({
|
||||||
|
id: va._id,
|
||||||
// Format projects with assigned VA details
|
firstName: va.userDetails?.firstName || '',
|
||||||
const projectsFormatted = await Promise.all(clientProjects.map(async (project) => {
|
lastName: va.userDetails?.lastName || '',
|
||||||
const assignedVAIds = project.vaAssigned || [];
|
email: va.userDetails?.email || '',
|
||||||
|
status: va.status || ''
|
||||||
// Get VA details and check activity for each assigned VA
|
|
||||||
const assignedVADetails = await Promise.all(assignedVAIds.map(async (vaId) => {
|
|
||||||
const va = vaMap[vaId.toString()];
|
|
||||||
if (!va) return null;
|
|
||||||
|
|
||||||
// Check if this VA is currently active on THIS specific project
|
|
||||||
const latestLog = await LoggedModel.findOne({
|
|
||||||
vaId: vaId,
|
|
||||||
projectId: project._id,
|
|
||||||
loggedType: "LOGIN"
|
|
||||||
}).sort({ time: -1 }).lean();
|
|
||||||
|
|
||||||
let isActive = false;
|
|
||||||
if (latestLog) {
|
|
||||||
const loginTime = new Date(latestLog.time);
|
|
||||||
const hoursSinceLastActivity = (currentTime - loginTime) / (1000 * 60 * 60);
|
|
||||||
if (hoursSinceLastActivity <= 24) {
|
|
||||||
isActive = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: va._id,
|
|
||||||
firstName: va.userDetails?.firstName || '',
|
|
||||||
lastName: va.userDetails?.lastName || '',
|
|
||||||
email: va.userDetails?.email || '',
|
|
||||||
status: va.status || '',
|
|
||||||
isActive: isActive
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Filter out null entries and count active VAs
|
|
||||||
const validVADetails = assignedVADetails.filter(Boolean);
|
|
||||||
const activeVAsCount = validVADetails.filter(va => va.isActive).length;
|
|
||||||
|
|
||||||
const dayNumber = getDayNumber();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: project._id,
|
|
||||||
name: project.name,
|
|
||||||
hourlyRate: project.hourlyRate,
|
|
||||||
vaAssigned: validVADetails.length,
|
|
||||||
activeVAs: activeVAsCount,
|
|
||||||
dedicatedHours: formatHoursToReadable(project.dedicatedHours[dayNumber]) || "0h",
|
|
||||||
status: project.status || '',
|
|
||||||
assignedVADetails: validVADetails
|
|
||||||
};
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Calculate summary counts
|
// Format projects with only required fields
|
||||||
const totalProjects = projectsFormatted.length;
|
const projectsFormatted = clientProjects.map((project) => ({
|
||||||
|
id: project._id,
|
||||||
// Count total unique VAs across all projects
|
name: project.name,
|
||||||
const allVAsAcrossProjects = new Set();
|
hourlyRate: project.hourlyRate,
|
||||||
projectsFormatted.forEach(project => {
|
vaAssigned: project.vaAssigned.length || 0,
|
||||||
project.assignedVADetails.forEach(va => {
|
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || 0,
|
||||||
allVAsAcrossProjects.add(va.id);
|
status: project.status || ''
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
// Count total active VAs (across all projects)
|
|
||||||
const totalActiveVAs = new Set();
|
|
||||||
projectsFormatted.forEach(project => {
|
|
||||||
project.assignedVADetails.forEach(va => {
|
|
||||||
if (va.isActive) {
|
|
||||||
totalActiveVAs.add(va.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectsWithActiveVAs = projectsFormatted.filter(project => project.activeVAs > 0).length;
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
vas: vasFormatted,
|
||||||
projects: projectsFormatted
|
projects: projectsFormatted
|
||||||
},
|
},
|
||||||
counts: {
|
counts: {
|
||||||
totalProjects: totalProjects,
|
totalVAs: vasFormatted.length,
|
||||||
totalVAs: allVAsAcrossProjects.size,
|
totalProjects: projectsFormatted.length
|
||||||
activeVAs: totalActiveVAs.size,
|
|
||||||
projectsWithActiveVAs: projectsWithActiveVAs
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Client dashboard projects error:", error);
|
console.error("Client dashboard error:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Internal Server Error",
|
message: "Internal Server Error",
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { ClientModel } from "../../../models/vaModel.js";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
async function client_logout_callback(req, res) {
|
|
||||||
try {
|
|
||||||
const accessToken = req.headers.token;
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ success: false, message: "Missing token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 clientEmail = decoded.email;
|
|
||||||
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
|
|
||||||
if (!clientUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Client not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find user by email
|
|
||||||
const user = await ClientModel.findOne({ "userDetails.email": clientEmail });
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({ success: false, message: "Invalid credentials" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await ClientModel.updateOne({ "userDetails.email": clientEmail }, {
|
|
||||||
$set: { accessToken: null, refreshToken: null }
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return accessToken and user info
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: "Log Out Successfull",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("client logout error:", error);
|
|
||||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { client_logout_callback };
|
|
||||||
|
|
@ -1,325 +1,82 @@
|
||||||
import VaClientRepository from "../../repository/vaClient.repository.js";
|
import VaClientRepository from "../../repository/vaClient.repository.js";
|
||||||
import VaClientService from "../../services/vaClient.service.js";
|
import VaClientService from "../../services/vaClient.service.js";
|
||||||
import logger from "../../config/logger.js";
|
import logger from "../../config/logger.js";
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import PasswordResetService from "../../services/passwordReset.service.js";
|
|
||||||
import PasswordResetRepository from "../../repository/passwordReset.repository.js";
|
|
||||||
|
|
||||||
|
|
||||||
const vaClientService = new VaClientService(new VaClientRepository());
|
const vaClientService = new VaClientService(new VaClientRepository());
|
||||||
|
|
||||||
const passwordResetService = new PasswordResetService(
|
|
||||||
new PasswordResetRepository(),
|
|
||||||
// new EmailService({
|
|
||||||
// host: process.env.SMTP_HOST,
|
|
||||||
// port: process.env.SMTP_PORT,
|
|
||||||
// user: process.env.SMTP_USER,
|
|
||||||
// pass: process.env.SMTP_PASS,
|
|
||||||
// from: process.env.EMAIL_FROM,
|
|
||||||
// })
|
|
||||||
);
|
|
||||||
|
|
||||||
async function va_register_client(req, res) {
|
async function va_register_client(req, res) {
|
||||||
logger.info("ClientController - Register Client Hit ...");
|
logger.info("ClientController - Register Client Hit ...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = req.body || {};
|
const payload = req.body || {};
|
||||||
const { userDetails } = payload;
|
const { userDetails } = payload;
|
||||||
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.lastName || !userDetails.password || !userDetails.name) {
|
if (!userDetails || !userDetails.email || !userDetails.name) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Missing required fields: firstName, lastName, email, password, and company name are required"
|
message: "Missing required fields: name and email are required"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await vaClientService.registerClient(payload);
|
const created = await vaClientService.registerClient(payload);
|
||||||
|
|
||||||
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
|
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
|
||||||
await passwordResetService.requestPasswordReset(userDetails.email, 'client');
|
return res.status(201).json({
|
||||||
logger.info(`Client password reset requested for: ${userDetails.email}`);
|
success: true,
|
||||||
|
|
||||||
return res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: created,
|
data: created,
|
||||||
message: "Client registered successfully"
|
message: "Client registered successfully"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`ClientController - Register error: ${error.message}`);
|
logger.error(`ClientController - Register error: ${error.message}`);
|
||||||
|
|
||||||
if (error.message === "User already exists with this email") {
|
if (error.message === "User already exists with this email") {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message
|
message: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.message.includes("Salesforce API is unreachable")) {
|
if (error.message.includes("Salesforce API is unreachable")) {
|
||||||
return res.status(503).json({
|
return res.status(503).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Salesforce service is currently unavailable. Please try again later."
|
message: "Salesforce service is currently unavailable. Please try again later."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Internal Server Error"
|
message: "Internal Server Error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional controller methods can be added here
|
// Additional controller methods can be added here
|
||||||
async function get_client_profile(req, res) {
|
async function get_client_by_email(req, res) {
|
||||||
try {
|
try {
|
||||||
const email = req.body.clientEmail;
|
const { email } = req.params;
|
||||||
const client = await vaClientService.getClientByEmail(email);
|
const client = await vaClientService.getClientByEmail(email);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Client not found"
|
message: "Client not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: client
|
data: client
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`ClientController - Get client error: ${error.message}`);
|
logger.error(`ClientController - Get client error: ${error.message}`);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Internal Server Error"
|
message: "Internal Server Error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function delete_client(req, res) {
|
export {
|
||||||
logger.info("ClientController - Delete Client Hit ...");
|
va_register_client,
|
||||||
|
get_client_by_email
|
||||||
try {
|
|
||||||
const { clientId } = req.params;
|
|
||||||
|
|
||||||
if (!clientId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Client ID is required"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedClient = await vaClientService.deleteClient(clientId);
|
|
||||||
|
|
||||||
if (!deletedClient) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: "Client not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`ClientController - Client deleted successfully: ${clientId}`);
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: "Client deleted successfully",
|
|
||||||
data: {
|
|
||||||
clientId: clientId,
|
|
||||||
clientName: deletedClient.userDetails?.name || 'Unknown Client',
|
|
||||||
deletedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`ClientController - Delete error: ${error.message}`);
|
|
||||||
|
|
||||||
if (error.message === "Client not found") {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "Internal Server Error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function update_client(req, res) {
|
|
||||||
logger.info("ClientController - Update Client Hit ...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { clientId } = req.params;
|
|
||||||
const payload = req.body || {};
|
|
||||||
const { userDetails } = payload;
|
|
||||||
|
|
||||||
if (!clientId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Client ID is required"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userDetails) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Missing userDetails in request body"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedClient = await vaClientService.updateClient(clientId, payload);
|
|
||||||
|
|
||||||
logger.info(`ClientController - Client updated successfully: ${clientId}`);
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: "Client updated successfully",
|
|
||||||
data: updatedClient
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`ClientController - Update error: ${error.message}`);
|
|
||||||
|
|
||||||
if (error.message === "Client not found") {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes("Email already exists")) {
|
|
||||||
return res.status(409).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function get_client_dashboard_profile(req, res) {
|
|
||||||
try {
|
|
||||||
const accessToken = req.headers.token;
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ success: false, message: "Missing token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 clientEmail = decoded.email;
|
|
||||||
|
|
||||||
// Use the new method that returns only necessary fields
|
|
||||||
const client = await vaClientService.getClientProfileByEmail(clientEmail);
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: "Client not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format the response to only include userDetails
|
|
||||||
const profileData = {
|
|
||||||
name: client.userDetails?.name || '',
|
|
||||||
email: client.userDetails?.email || '',
|
|
||||||
address: client.userDetails?.address || '',
|
|
||||||
city: client.userDetails?.city || '',
|
|
||||||
state: client.userDetails?.state || '',
|
|
||||||
zip: client.userDetails?.zip || '',
|
|
||||||
position: client.userDetails?.position || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: profileData
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`ClientController - Get dashboard profile error: ${error.message}`);
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "Internal Server Error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function update_client_password(req, res) {
|
|
||||||
logger.info("ClientController - Update Client Password Hit ...");
|
|
||||||
console.log("Update Client Password - Request Body:", req.body);
|
|
||||||
try {
|
|
||||||
const accessToken = req.headers.token;
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ success: false, message: "Missing token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 clientEmail = decoded.email;
|
|
||||||
const payload = req.body || {};
|
|
||||||
const { userDetails } = payload;
|
|
||||||
|
|
||||||
// Get client by email to get the ID
|
|
||||||
const client = await vaClientService.getClientByEmail(clientEmail);
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: "Client not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientId = client._id;
|
|
||||||
|
|
||||||
// Call service to update password
|
|
||||||
const result = await vaClientService.updateClientPassword(clientId, userDetails);
|
|
||||||
|
|
||||||
logger.info(`ClientController - Client password updated successfully: ${clientEmail}`);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: "Password updated successfully",
|
|
||||||
data: result
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`ClientController - Update password error: ${error.message}`);
|
|
||||||
|
|
||||||
if (error.message === "Client not found") {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes("Current password is incorrect") ||
|
|
||||||
error.message.includes("New passwords do not match") ||
|
|
||||||
error.message.includes("All password fields are required")) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "Internal Server Error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
va_register_client,
|
|
||||||
get_client_profile,
|
|
||||||
delete_client,
|
|
||||||
update_client,
|
|
||||||
get_client_dashboard_profile,
|
|
||||||
update_client_password
|
|
||||||
};
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { ClientModel, VAModel } from "../../models/vaModel.js";
|
import { ClientModel } from "../../models/vaModel.js";
|
||||||
|
|
||||||
// Login VA
|
// Login VA
|
||||||
async function va_login_client(req, res) {
|
async function va_login_client(req, res) {
|
||||||
|
|
@ -13,17 +13,8 @@ async function va_login_client(req, res) {
|
||||||
return res.status(400).json({ success: false, message: "Please provide email and password" });
|
return res.status(400).json({ success: false, message: "Please provide email and password" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let isVa = false;
|
|
||||||
|
|
||||||
// Find user by email
|
// Find user by email
|
||||||
let user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
|
const user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
|
||||||
const vaUser = await VAModel.findOne({ "userDetails.email": userDetails.email });
|
|
||||||
|
|
||||||
if (vaUser) {
|
|
||||||
isVa = true
|
|
||||||
user = vaUser
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ success: false, message: "Invalid credentials" });
|
return res.status(404).json({ success: false, message: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +49,6 @@ async function va_login_client(req, res) {
|
||||||
id: user._id,
|
id: user._id,
|
||||||
email: user.userDetails.email,
|
email: user.userDetails.email,
|
||||||
name: user.userDetails.firstName,
|
name: user.userDetails.firstName,
|
||||||
userType: isVa ? "VA" : "CLIENT",
|
|
||||||
},
|
},
|
||||||
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
|
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
|
||||||
{ expiresIn: "30d" }
|
{ expiresIn: "30d" }
|
||||||
|
|
@ -79,7 +69,6 @@ async function va_login_client(req, res) {
|
||||||
name: user.userDetails.firstName,
|
name: user.userDetails.firstName,
|
||||||
email: user.userDetails.email,
|
email: user.userDetails.email,
|
||||||
},
|
},
|
||||||
userType: isVa ? "VA" : "CLIENT",
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("va_client_login_register error:", error);
|
console.error("va_client_login_register error:", error);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import PasswordResetRepository from "../repository/passwordReset.repository.js";
|
import PasswordResetRepository from "../repository/passwordReset.repository.js";
|
||||||
import PasswordResetService from "../services/passwordReset.service.js";
|
import PasswordResetService from "../services/passwordReset.service.js";
|
||||||
// import EmailService from "../services/email.service.js";
|
import EmailService from "../services/email.service.js";
|
||||||
import logger from "../config/logger.js";
|
import logger from "../config/logger.js";
|
||||||
|
|
||||||
const passwordResetService = new PasswordResetService(
|
const passwordResetService = new PasswordResetService(
|
||||||
new PasswordResetRepository(),
|
new PasswordResetRepository(),
|
||||||
// new EmailService({
|
new EmailService({
|
||||||
// host: process.env.SMTP_HOST,
|
host: process.env.SMTP_HOST,
|
||||||
// port: process.env.SMTP_PORT,
|
port: process.env.SMTP_PORT,
|
||||||
// user: process.env.SMTP_USER,
|
user: process.env.SMTP_USER,
|
||||||
// pass: process.env.SMTP_PASS,
|
pass: process.env.SMTP_PASS,
|
||||||
// from: process.env.EMAIL_FROM,
|
from: process.env.EMAIL_FROM,
|
||||||
// })
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// VA: Request password reset
|
// VA: Request password reset
|
||||||
|
|
@ -55,50 +55,26 @@ async function render_reset_form(req, res) {
|
||||||
try {
|
try {
|
||||||
const { token, userType } = req.query;
|
const { token, userType } = req.query;
|
||||||
|
|
||||||
// ❌ Missing params
|
|
||||||
if (!token || !userType) {
|
if (!token || !userType) {
|
||||||
return res.render('auth/reset-password-error', {
|
return res.render('auth/reset-password-error', { error: 'Reset token or user type is missing', userType: userType || 'va' });
|
||||||
title: "Reset Password",
|
|
||||||
layout: 'layouts/auth',
|
|
||||||
error: 'Reset token or user type is missing',
|
|
||||||
userType: userType || 'va'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Validate token
|
// Validate token
|
||||||
const validation = await passwordResetService.validateResetToken(token, userType);
|
const validation = await passwordResetService.validateResetToken(token, userType);
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return res.render('auth/reset-password-error', {
|
return res.render('auth/reset-password-error', { error: validation.message, userType: userType || 'va' });
|
||||||
title: "Reset Password",
|
|
||||||
layout: 'layouts/auth',
|
|
||||||
error: validation.message,
|
|
||||||
userType: userType || 'va'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Render reset password form
|
// Render reset form with token
|
||||||
return res.render('auth/reset-password-form', {
|
res.render('auth/reset-password-form', { token, userType, error: null });
|
||||||
title: "Reset Password",
|
|
||||||
layout: 'layouts/auth',
|
|
||||||
token,
|
|
||||||
userType,
|
|
||||||
error: null
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`PasswordResetController - Render form error: ${error.message}`);
|
logger.error(`PasswordResetController - Render form error: ${error.message}`);
|
||||||
|
res.render('auth/reset-password-error', { error: 'Failed to load reset form' });
|
||||||
return res.render('auth/reset-password-error', {
|
|
||||||
title: "Reset Password",
|
|
||||||
layout: 'layouts/auth',
|
|
||||||
error: 'Failed to load reset form',
|
|
||||||
userType: 'va'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Process password reset form submission (shared for both VA and Client)
|
// Process password reset form submission (shared for both VA and Client)
|
||||||
async function process_reset(req, res) {
|
async function process_reset(req, res) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
import { ProjectModel, VAModel, ClientModel } from "../../models/vaModel.js";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete project and all related data (without transaction)
|
|
||||||
*/
|
|
||||||
async function delete_project_sf(req, res) {
|
|
||||||
try {
|
|
||||||
const { projectId } = req.params;
|
|
||||||
|
|
||||||
// Validate projectId
|
|
||||||
if (!projectId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Project ID is required"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let projectObjectId;
|
|
||||||
try {
|
|
||||||
projectObjectId = new Types.ObjectId(projectId);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Invalid Project ID format"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if project exists
|
|
||||||
const existingProject = await ProjectModel.findById(projectObjectId);
|
|
||||||
if (!existingProject) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: "Project not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientId = existingProject.clientId;
|
|
||||||
const vaAssigned = existingProject.vaAssigned || [];
|
|
||||||
const projectName = existingProject.name;
|
|
||||||
|
|
||||||
console.log(`Deleting project: ${projectName} (${projectId})`);
|
|
||||||
|
|
||||||
const deletionSummary = {};
|
|
||||||
|
|
||||||
// 1. Remove project from Client's projectIds array
|
|
||||||
if (clientId) {
|
|
||||||
await ClientModel.findByIdAndUpdate(
|
|
||||||
clientId,
|
|
||||||
{ $pull: { projectIds: projectObjectId } }
|
|
||||||
);
|
|
||||||
deletionSummary.clientUpdated = true;
|
|
||||||
console.log(`Removed project from client`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Remove project from all assigned VAs' projectIds arrays
|
|
||||||
if (vaAssigned.length > 0) {
|
|
||||||
const vaResult = await VAModel.updateMany(
|
|
||||||
{ _id: { $in: vaAssigned.vaId } },
|
|
||||||
{ $pull: { projectIds: projectObjectId } }
|
|
||||||
);
|
|
||||||
deletionSummary.vasUpdated = vaResult.modifiedCount;
|
|
||||||
console.log(`Removed project from ${deletionSummary.vasUpdated} VAs`);
|
|
||||||
} else {
|
|
||||||
deletionSummary.vasUpdated = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Finally delete the project
|
|
||||||
await ProjectModel.findByIdAndDelete(projectObjectId);
|
|
||||||
|
|
||||||
console.log(`✅ Successfully deleted project: ${projectName} (${projectId})`);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Project and all related data deleted successfully',
|
|
||||||
data: {
|
|
||||||
projectId: projectId,
|
|
||||||
projectName: projectName,
|
|
||||||
clientId: clientId,
|
|
||||||
deletedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("delete_project_sf error:", error);
|
|
||||||
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "Failed to delete project",
|
|
||||||
error: process.env.NODE_ENV === 'production'
|
|
||||||
? 'Internal server error'
|
|
||||||
: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { delete_project_sf };
|
|
||||||
|
|
@ -4,21 +4,21 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
|
||||||
// Register a new VA
|
// Register a new VA
|
||||||
async function project_add_sf(req, res) {
|
async function project_add_sf(req, res) {
|
||||||
try {
|
try {
|
||||||
const payload = req.body || {};
|
const payload = req.body|| {};
|
||||||
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
|
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Project name is required"
|
message: "Project name is required"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Client ID is required"
|
message: "Client ID is required"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,62 +27,48 @@ async function project_add_sf(req, res) {
|
||||||
try {
|
try {
|
||||||
clientObjectId = new Types.ObjectId(clientId);
|
clientObjectId = new Types.ObjectId(clientId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid Client ID format"
|
message: "Invalid Client ID format"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and convert vaAssigned to array of ObjectIds
|
// Validate and convert vaAssigned to array of ObjectIds
|
||||||
// let vaAssignedArray = [];
|
let vaAssignedArray = [];
|
||||||
// if (vaAssigned) {
|
if (vaAssigned) {
|
||||||
// if (Array.isArray(vaAssigned)) {
|
if (Array.isArray(vaAssigned)) {
|
||||||
// try {
|
try {
|
||||||
// vaAssignedArray = vaAssigned.map(vaId => {
|
vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
|
||||||
// if(typeof vaId === 'object' && "isFlexibleAllowed" in vaId){
|
} catch (error) {
|
||||||
// // Handle Error form SF Side for now - That vaId is coming as object
|
return res.status(400).json({
|
||||||
// return new Types.ObjectId(vaId.vaId)
|
success: false,
|
||||||
// } else {
|
message: "Invalid VA ID format in vaAssigned array"
|
||||||
// return new Types.ObjectId(vaId)
|
});
|
||||||
// }
|
}
|
||||||
// });
|
} else if (typeof vaAssigned === 'string') {
|
||||||
// } catch (error) {
|
// Handle single VA ID as string
|
||||||
// return res.status(400).json({
|
try {
|
||||||
// success: false,
|
vaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
||||||
// message: "Invalid VA ID format in vaAssigned array"
|
} catch (error) {
|
||||||
// });
|
return res.status(400).json({
|
||||||
// }
|
success: false,
|
||||||
// } else if (typeof vaAssigned === 'string') {
|
message: "Invalid VA ID format"
|
||||||
// // Handle single VA ID as string
|
});
|
||||||
// try {
|
}
|
||||||
// vaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
}
|
||||||
// } catch (error) {
|
}
|
||||||
// return res.status(400).json({
|
|
||||||
// success: false,
|
|
||||||
// message: "Invalid VA ID format"
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// sanitize/construct allowed fields
|
// sanitize/construct allowed fields
|
||||||
const toCreate = {
|
const toCreate = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
hourlyRate: hourlyRate || '0',
|
hourlyRate: hourlyRate || '0',
|
||||||
clientId: clientObjectId,
|
clientId: clientObjectId,
|
||||||
vaAssigned: vaAssigned,
|
vaAssigned: vaAssignedArray,
|
||||||
dedicatedHours: dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"],
|
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
|
||||||
vaWorkingScheduleStart: vaWorkingScheduleStart || null,
|
|
||||||
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
|
|
||||||
isFlexibleAllowed: isFlexibleAllowed || true,
|
|
||||||
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
|
|
||||||
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const created = await ProjectModel.create(toCreate);
|
const created = await ProjectModel.create(toCreate);
|
||||||
|
|
||||||
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
|
|
||||||
|
|
||||||
const relationshipUpdate = await updateEntityRelationships({
|
const relationshipUpdate = await updateEntityRelationships({
|
||||||
project: {
|
project: {
|
||||||
projectId: created._id,
|
projectId: created._id,
|
||||||
|
|
@ -95,8 +81,36 @@ async function project_add_sf(req, res) {
|
||||||
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
|
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
// Send data to Salesforce API after successful registration
|
||||||
|
try {
|
||||||
|
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`;
|
||||||
|
|
||||||
|
// const response = await fetch(salesforceUrl, {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify(created)
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// const errorText = await response.text();
|
||||||
|
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const result = await response.json();
|
||||||
|
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
|
|
||||||
|
// res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
||||||
|
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to sync with Salesforce:", error.message);
|
||||||
|
|
||||||
|
// Create a custom error to distinguish Salesforce errors
|
||||||
|
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
|
salesforceError.isSalesforceError = true;
|
||||||
|
throw salesforceError;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("project_add_sf error:", error);
|
console.error("project_add_sf error:", error);
|
||||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,20 @@ async function update_project_sf(req, res) {
|
||||||
try {
|
try {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
const payload = req.body || {};
|
const payload = req.body || {};
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
hourlyRate,
|
hourlyRate,
|
||||||
vaAssigned,
|
vaAssigned,
|
||||||
dedicatedHours,
|
dedicatedHours,
|
||||||
status,
|
status,
|
||||||
totalLoggedHours,
|
totalLoggedHours
|
||||||
vaWorkingScheduleStart,
|
|
||||||
vaWorkingScheduleEnd,
|
|
||||||
isFlexibleAllowed,
|
|
||||||
vaWorkingDelayAllowedStart,
|
|
||||||
vaWorkingDelayAllowedEnd
|
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
// Validate projectId
|
// Validate projectId
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Project ID is required"
|
message: "Project ID is required"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,18 +31,18 @@ async function update_project_sf(req, res) {
|
||||||
try {
|
try {
|
||||||
projectObjectId = new Types.ObjectId(projectId);
|
projectObjectId = new Types.ObjectId(projectId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid Project ID format"
|
message: "Invalid Project ID format"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if project exists
|
// Check if project exists
|
||||||
const existingProject = await ProjectModel.findById(projectObjectId);
|
const existingProject = await ProjectModel.findById(projectObjectId);
|
||||||
if (!existingProject) {
|
if (!existingProject) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Project not found"
|
message: "Project not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,9 +54,9 @@ async function update_project_sf(req, res) {
|
||||||
// Update name if provided
|
// Update name if provided
|
||||||
if (name !== undefined) {
|
if (name !== undefined) {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Project name cannot be empty"
|
message: "Project name cannot be empty"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
updateData.name = name.trim();
|
updateData.name = name.trim();
|
||||||
|
|
@ -74,7 +69,7 @@ async function update_project_sf(req, res) {
|
||||||
|
|
||||||
// Update dedicated hours if provided
|
// Update dedicated hours if provided
|
||||||
if (dedicatedHours !== undefined) {
|
if (dedicatedHours !== undefined) {
|
||||||
updateData.dedicatedHours = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"];
|
updateData.dedicatedHours = parseFloat(dedicatedHours) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total logged hours if provided
|
// Update total logged hours if provided
|
||||||
|
|
@ -85,81 +80,49 @@ async function update_project_sf(req, res) {
|
||||||
// Update status if provided
|
// Update status if provided
|
||||||
if (status !== undefined) {
|
if (status !== undefined) {
|
||||||
if (!["ACTIVE", "INACTIVE"].includes(status)) {
|
if (!["ACTIVE", "INACTIVE"].includes(status)) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Status must be either ACTIVE or INACTIVE"
|
message: "Status must be either ACTIVE or INACTIVE"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
updateData.status = status;
|
updateData.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update VA working schedule if provided
|
|
||||||
if (vaWorkingScheduleStart !== undefined) {
|
|
||||||
updateData.vaWorkingScheduleStart = vaWorkingScheduleStart || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vaWorkingScheduleEnd !== undefined) {
|
|
||||||
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFlexibleAllowed !== undefined) {
|
|
||||||
updateData.isFlexibleAllowed = isFlexibleAllowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vaWorkingDelayAllowedStart !== undefined) {
|
|
||||||
updateData.vaWorkingDelayAllowedStart = vaWorkingDelayAllowedStart || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vaWorkingDelayAllowedEnd !== undefined) {
|
|
||||||
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vaAssigned !== undefined && vaAssigned !== null && Array.isArray(vaAssigned)) {
|
|
||||||
updateData.vaAssigned = vaAssigned;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle VA assignments - this requires relationship updates
|
// Handle VA assignments - this requires relationship updates
|
||||||
if (vaAssigned !== undefined) {
|
if (vaAssigned !== undefined) {
|
||||||
let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
|
let newVaAssignedArray = [];
|
||||||
|
|
||||||
// Parse and validate VA IDs
|
// Parse and validate VA IDs
|
||||||
if (vaAssigned && vaAssigned.length > 0) {
|
if (vaAssigned && vaAssigned.length > 0) {
|
||||||
if (Array.isArray(vaAssigned)) {
|
if (Array.isArray(vaAssigned)) {
|
||||||
try {
|
try {
|
||||||
newVaAssignedArray = vaAssigned.map(vaId => {
|
newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
|
||||||
if (typeof vaId === 'object' && "isFlexibleAllowed" in vaId) {
|
|
||||||
// Handle Error form SF Side for now - That vaId is coming as object
|
|
||||||
return new Types.ObjectId(vaId.vaId)
|
|
||||||
} else {
|
|
||||||
return new Types.ObjectId(vaId)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid VA ID format in vaAssigned array"
|
message: "Invalid VA ID format in vaAssigned array"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (typeof vaAssigned === 'string') {
|
} else if (typeof vaAssigned === 'string') {
|
||||||
try {
|
try {
|
||||||
newVaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
newVaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid VA ID format"
|
message: "Invalid VA ID format"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateData.vaAssigned = newVaAssignedArray;
|
updateData.vaAssigned = newVaAssignedArray;
|
||||||
|
|
||||||
// Calculate VAs to add and remove for relationship updates
|
// Calculate VAs to add and remove for relationship updates
|
||||||
const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.toString());
|
const currentVaIds = existingProject.vaAssigned.map(va => va.toString());
|
||||||
const newVaIds = newVaAssignedArray.map(va => va.toString());
|
const newVaIds = newVaAssignedArray.map(va => va.toString());
|
||||||
|
|
||||||
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
|
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
|
||||||
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.vaId.toString()));
|
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString()));
|
||||||
|
|
||||||
// Update relationships for added VAs
|
// Update relationships for added VAs
|
||||||
if (vaToAdd.length > 0) {
|
if (vaToAdd.length > 0) {
|
||||||
|
|
@ -186,9 +149,9 @@ async function update_project_sf(req, res) {
|
||||||
|
|
||||||
// If no fields to update
|
// If no fields to update
|
||||||
if (Object.keys(updateData).length === 0) {
|
if (Object.keys(updateData).length === 0) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "No valid fields to update"
|
message: "No valid fields to update"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,7 +161,7 @@ async function update_project_sf(req, res) {
|
||||||
updateData,
|
updateData,
|
||||||
{ new: true, runValidators: true }
|
{ new: true, runValidators: true }
|
||||||
).populate('clientId', 'userDetails.name userDetails.email')
|
).populate('clientId', 'userDetails.name userDetails.email')
|
||||||
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
|
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
|
||||||
|
|
||||||
// If VA assignments were changed but relationship update failed, log warning
|
// If VA assignments were changed but relationship update failed, log warning
|
||||||
if (vaAssigned !== undefined) {
|
if (vaAssigned !== undefined) {
|
||||||
|
|
@ -222,33 +185,33 @@ async function update_project_sf(req, res) {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("update_project_sf error:", error);
|
console.error("update_project_sf error:", error);
|
||||||
|
|
||||||
// MongoDB duplicate key error
|
// MongoDB duplicate key error
|
||||||
if (error.code === 11000) {
|
if (error.code === 11000) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Project with this name already exists"
|
message: "Project with this name already exists"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// MongoDB validation error
|
// MongoDB validation error
|
||||||
if (error.name === 'ValidationError') {
|
if (error.name === 'ValidationError') {
|
||||||
const errors = Object.values(error.errors).map(err => err.message);
|
const errors = Object.values(error.errors).map(err => err.message);
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Validation error",
|
message: "Validation error",
|
||||||
errors: errors
|
errors: errors
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Internal Server Error",
|
message: "Internal Server Error",
|
||||||
error: error.message
|
error: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
update_project_sf
|
update_project_sf
|
||||||
};
|
};
|
||||||
|
|
@ -1,102 +1,101 @@
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
|
import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
// function to close open work tracks and create logout
|
// function to close open work tracks and create logout
|
||||||
// async function closePreviousSession(vaId, lastLog) {
|
async function closePreviousSession(vaId, lastLog) {
|
||||||
// try {
|
try {
|
||||||
// const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
// // Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
|
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
|
||||||
// const openWorkTracks = await WorkTrackModel.find({
|
const openWorkTracks = await WorkTrackModel.find({
|
||||||
// vaId,
|
vaId,
|
||||||
// logId: lastLog._id,
|
logId: lastLog._id,
|
||||||
// $or: [
|
$or: [
|
||||||
// { to: { $exists: false } },
|
{ to: { $exists: false } },
|
||||||
// { totalTime: 0 },
|
{ totalTime: 0 },
|
||||||
// { to: null }
|
{ to: null }
|
||||||
// ]
|
]
|
||||||
// });
|
});
|
||||||
|
|
||||||
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
|
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
|
||||||
|
|
||||||
// // Close all open work tracks
|
// Close all open work tracks
|
||||||
// for (const workTrack of openWorkTracks) {
|
for (const workTrack of openWorkTracks) {
|
||||||
// const fromTime = new Date(workTrack.from);
|
const fromTime = new Date(workTrack.from);
|
||||||
// const totalTimeMs = currentTime - fromTime;
|
const totalTimeMs = currentTime - fromTime;
|
||||||
|
|
||||||
// // Update the work track with end time and total time
|
// Update the work track with end time and total time
|
||||||
// workTrack.to = currentTime;
|
workTrack.to = currentTime;
|
||||||
// workTrack.totalTime = totalTimeMs;
|
workTrack.totalTime = totalTimeMs;
|
||||||
// await workTrack.save();
|
await workTrack.save();
|
||||||
|
|
||||||
// // console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
|
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Create a logout log entry
|
// Create a logout log entry
|
||||||
// const logoutLog = await LoggedModel.create({
|
const logoutLog = await LoggedModel.create({
|
||||||
// loggedType: "LOGOUT",
|
loggedType: "LOGOUT",
|
||||||
// vaId,
|
vaId,
|
||||||
// vaEmail: lastLog.vaEmail, // Use email from previous log
|
vaEmail: lastLog.vaEmail, // Use email from previous log
|
||||||
// projectId: lastLog.projectId,
|
projectId: lastLog.projectId,
|
||||||
// time: currentTime // Use same timestamp for consistent logging
|
time: currentTime // Use same timestamp for consistent logging
|
||||||
// });
|
});
|
||||||
|
|
||||||
// // console.log(`Created logout log: ${logoutLog._id}`);
|
// console.log(`Created logout log: ${logoutLog._id}`);
|
||||||
// try {
|
// try {
|
||||||
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||||
// const sfDataSync = {
|
// const sfDataSync = {
|
||||||
// "_id": logoutLog._id,
|
// "_id": logoutLog._id,
|
||||||
// "loggedType": "LOGOUT",
|
// "loggedType": "LOGOUT",
|
||||||
// "vaId": vaId,
|
// "vaId": vaId,
|
||||||
// "vaEmail": lastLog.vaEmail,
|
// "vaEmail": lastLog.vaEmail,
|
||||||
// "projectId": lastLog.projectId,
|
// "projectId": lastLog.projectId,
|
||||||
// "time": logoutLog.time,
|
// "time": logoutLog.time,
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const response = await authFetch(salesforceUrl, {
|
// const response = await fetch(salesforceUrl, {
|
||||||
// method: 'POST',
|
// method: 'POST',
|
||||||
// headers: {
|
// headers: {
|
||||||
// 'Content-Type': 'application/json',
|
// 'Content-Type': 'application/json',
|
||||||
// // Add any required headers for Salesforce authentication
|
// // Add any required headers for Salesforce authentication
|
||||||
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
// },
|
// },
|
||||||
// body: JSON.stringify(sfDataSync)
|
// body: JSON.stringify(sfDataSync)
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// if (!response.ok) {
|
// if (!response.ok) {
|
||||||
// const errorText = await response.text();
|
// const errorText = await response.text();
|
||||||
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const result = await response.json();
|
// const result = await response.json();
|
||||||
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
|
|
||||||
// return {
|
// return {
|
||||||
// closedWorkTracks: openWorkTracks.length,
|
// closedWorkTracks: openWorkTracks.length,
|
||||||
// logoutLogId: logoutLog._id,
|
// logoutLogId: logoutLog._id,
|
||||||
// SyncSalesforce: result
|
// SyncSalesforce: result
|
||||||
// };
|
// };
|
||||||
// } catch (error) {
|
// } catch (error) {
|
||||||
// console.error("Failed to sync with Salesforce:", error.message);
|
// console.error("Failed to sync with Salesforce:", error.message);
|
||||||
// console.log(error)
|
// console.log(error)
|
||||||
|
|
||||||
// // Create a custom error to distinguish Salesforce errors
|
// // Create a custom error to distinguish Salesforce errors
|
||||||
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
// salesforceError.isSalesforceError = true;
|
// salesforceError.isSalesforceError = true;
|
||||||
// throw salesforceError;
|
// throw salesforceError;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // return {
|
return {
|
||||||
// // closedWorkTracks: openWorkTracks.length,
|
closedWorkTracks: openWorkTracks.length,
|
||||||
// // logoutLogId: logoutLog._id,
|
logoutLogId: logoutLog._id,
|
||||||
// // };
|
};
|
||||||
|
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error closing previous session:", error);
|
console.error("Error closing previous session:", error);
|
||||||
// throw error;
|
throw error;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Login VA
|
// Login VA
|
||||||
async function va_login_callback(req, res) {
|
async function va_login_callback(req, res) {
|
||||||
|
|
@ -132,17 +131,17 @@ async function va_login_callback(req, res) {
|
||||||
// also on main login when user try to login into app then logout project and workModel track then allow login on same timestamp
|
// also on main login when user try to login into app then logout project and workModel track then allow login on same timestamp
|
||||||
|
|
||||||
// Check if VA has any active sessions (LOGIN without LOGOUT)
|
// Check if VA has any active sessions (LOGIN without LOGOUT)
|
||||||
// const activeSessions = await LoggedModel.find({
|
const activeSessions = await LoggedModel.find({
|
||||||
// vaId: user._id
|
vaId: user._id
|
||||||
// }).sort({ time: -1 });
|
}).sort({ time: -1 });
|
||||||
|
|
||||||
// let closeResult = [];
|
let closeResult = [];
|
||||||
|
|
||||||
// if (activeSessions.length > 0) {
|
if (activeSessions.length > 0) {
|
||||||
// // Get the latest login to check if there's no corresponding logout
|
// Get the latest login to check if there's no corresponding logout
|
||||||
// const latestLog = activeSessions[0];
|
const latestLog = activeSessions[0];
|
||||||
// closeResult = await closePreviousSession(user._id, latestLog);
|
closeResult = await closePreviousSession(user._id, latestLog);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Generate JWT accessToken with 24h expiry
|
// Generate JWT accessToken with 24h expiry
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
|
|
@ -172,23 +171,23 @@ async function va_login_callback(req, res) {
|
||||||
|
|
||||||
let responseData = [];
|
let responseData = [];
|
||||||
|
|
||||||
// if (closeResult) {
|
if (closeResult) {
|
||||||
// responseData = {
|
responseData = {
|
||||||
// success: true,
|
success: true,
|
||||||
// message: "Login Successfull",
|
message: "Login Successfull",
|
||||||
// accessToken,
|
accessToken,
|
||||||
// refreshToken,
|
refreshToken,
|
||||||
// user: {
|
user: {
|
||||||
// id: user._id,
|
id: user._id,
|
||||||
// name: user.userDetails.firstName,
|
name: user.userDetails.firstName,
|
||||||
// email: user.userDetails.email,
|
email: user.userDetails.email,
|
||||||
// },
|
},
|
||||||
// previousSession: {
|
previousSession: {
|
||||||
// closedWorkTracks: closeResult.closedWorkTracks,
|
closedWorkTracks: closeResult.closedWorkTracks,
|
||||||
// logoutLogId: closeResult.logoutLogId
|
logoutLogId: closeResult.logoutLogId
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
responseData = {
|
responseData = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Login Successfull",
|
message: "Login Successfull",
|
||||||
|
|
@ -200,7 +199,7 @@ async function va_login_callback(req, res) {
|
||||||
email: user.userDetails.email,
|
email: user.userDetails.email,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Return accessToken and user info
|
// Return accessToken and user info
|
||||||
return res.status(200).json(responseData);
|
return res.status(200).json(responseData);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
|
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
// Apply for leave
|
// Apply for leave
|
||||||
async function va_leave_apply_function(req, res) {
|
async function va_leave_apply_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return res.status(401).json({ success: false, message: "Missing token" });
|
return res.status(401).json({ success: false, message: "Missing token" });
|
||||||
}
|
}
|
||||||
|
|
@ -26,9 +25,9 @@ async function va_leave_apply_function(req, res) {
|
||||||
const { dateFrom, dateTo, leaveType, comment } = req.body;
|
const { dateFrom, dateTo, leaveType, comment } = req.body;
|
||||||
|
|
||||||
if (!dateFrom || !dateTo || !leaveType || !comment) {
|
if (!dateFrom || !dateTo || !leaveType || !comment) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "dateFrom, dateTo, leaveType, and comment are required"
|
message: "dateFrom, dateTo, leaveType, and comment are required"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,49 +36,49 @@ async function va_leave_apply_function(req, res) {
|
||||||
try {
|
try {
|
||||||
startDate = new Date(dateFrom);
|
startDate = new Date(dateFrom);
|
||||||
endDate = new Date(dateTo);
|
endDate = new Date(dateTo);
|
||||||
|
|
||||||
// Set to start of day for accurate calculation
|
// Set to start of day for accurate calculation
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
endDate.setHours(0, 0, 0, 0);
|
endDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid date format. Use valid date format (YYYY-MM-DD)"
|
message: "Invalid date format. Use valid date format (YYYY-MM-DD)"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid date format"
|
message: "Invalid date format"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure start date is before or equal to end date
|
// Ensure start date is before or equal to end date
|
||||||
if (startDate > endDate) {
|
if (startDate > endDate) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Start date must be before or equal to end date"
|
message: "Start date must be before or equal to end date"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure dates are not in the past
|
// Ensure dates are not in the past
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
if (startDate < today) {
|
if (startDate < today) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Cannot apply for leave for past dates"
|
message: "Cannot apply for leave for past dates"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate leave type
|
// Validate leave type
|
||||||
const validLeaveTypes = ["SICK", "PTO"];
|
const validLeaveTypes = ["SICK", "PTO"];
|
||||||
if (!validLeaveTypes.includes(leaveType)) {
|
if (!validLeaveTypes.includes(leaveType)) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid leave type. Must be SICK or PTO"
|
message: "Invalid leave type. Must be SICK or PTO"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,9 +87,9 @@ async function va_leave_apply_function(req, res) {
|
||||||
const totalDays = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + 1; // +1 to include both start and end dates
|
const totalDays = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + 1; // +1 to include both start and end dates
|
||||||
|
|
||||||
if (totalDays <= 0) {
|
if (totalDays <= 0) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid date range"
|
message: "Invalid date range"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,9 +106,9 @@ async function va_leave_apply_function(req, res) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingLeave) {
|
if (existingLeave) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "You already have a pending leave request that overlaps with these dates"
|
message: "You already have a pending leave request that overlaps with these dates"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,13 +125,12 @@ async function va_leave_apply_function(req, res) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingApprovedLeave) {
|
if (existingApprovedLeave) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "You already have an approved leave that overlaps with these dates"
|
message: "You already have an approved leave that overlaps with these dates"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedComment = processHTMLComment(comment);
|
|
||||||
// Create leave request
|
// Create leave request
|
||||||
const leaveRequest = new LeavesModel({
|
const leaveRequest = new LeavesModel({
|
||||||
vaId,
|
vaId,
|
||||||
|
|
@ -140,74 +138,72 @@ async function va_leave_apply_function(req, res) {
|
||||||
dateTo: endDate,
|
dateTo: endDate,
|
||||||
days: totalDays.toString(),
|
days: totalDays.toString(),
|
||||||
leaveType,
|
leaveType,
|
||||||
comment: processedComment.processedHTML,
|
comment,
|
||||||
attachments: processedComment.extractedImages,
|
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
});
|
});
|
||||||
|
|
||||||
await leaveRequest.save();
|
await leaveRequest.save();
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/leave-request`;
|
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
||||||
const sfData = {
|
// const sfData = {
|
||||||
"leaveId": leaveRequest._id.toString(),
|
// "id": adjustmentRequest._id,
|
||||||
// "va": {
|
// "va": {
|
||||||
// "id": vaId
|
// "id": vaId
|
||||||
// },
|
// },
|
||||||
"va": vaId.toString(),
|
// "project": {
|
||||||
"dateFrom": startDate.toISOString().split('T')[0],
|
// "id": projectId
|
||||||
"dateTo": endDate.toISOString().split('T')[0],
|
// },
|
||||||
"totalDays": totalDays,
|
// "date": adjustmentDate.toISOString().split('T')[0],
|
||||||
"leaveType": leaveType,
|
// "loginTime": loginDateTime.toISOString(),
|
||||||
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
|
// "logoutTime": logoutDateTime.toISOString(),
|
||||||
"attachments": processedComment.extractedImages, // Send extracted images info
|
// "comment": comment,
|
||||||
"status": "PENDING",
|
// "status": "PENDING",
|
||||||
"rejectionReason": ""
|
// "rejectionReason": "",
|
||||||
}
|
// "adjustedHours": formatTime(adjustedSeconds)
|
||||||
|
// }
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
// const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
// method: 'POST',
|
||||||
headers: {
|
// headers: {
|
||||||
'Content-Type': 'application/json',
|
// 'Content-Type': 'application/json',
|
||||||
// Add any required headers for Salesforce authentication
|
// // Add any required headers for Salesforce authentication
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
},
|
// },
|
||||||
body: JSON.stringify({ "data": sfData })
|
// body: JSON.stringify({"data": sfData})
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// const errorText = await response.text();
|
||||||
|
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
|
// }
|
||||||
|
|
||||||
if (!response.ok) {
|
// const result = await response.json();
|
||||||
const errorText = await response.text();
|
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
await LeavesModel.findByIdAndDelete(leaveRequest._id)
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
// return res.status(201).json({
|
||||||
}
|
// success: true,
|
||||||
|
// message: "Adjustment request submitted successfully",
|
||||||
// const result = await response.json();
|
// data: {
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
// adjustmentId: adjustmentRequest._id,
|
||||||
|
// date: adjustmentDate.toISOString().split('T')[0],
|
||||||
// return res.status(201).json({
|
// loginTime: loginDateTime.toISOString(),
|
||||||
// success: true,
|
// logoutTime: logoutDateTime.toISOString(),
|
||||||
// message: "Leave request submitted successfully",
|
// adjustedHours: formatTime(adjustedSeconds),
|
||||||
// data: {
|
// comment: comment,
|
||||||
// leaveId: leaveRequest._id,
|
// status: "PENDING"
|
||||||
// dateFrom: startDate.toISOString().split('T')[0],
|
// },
|
||||||
// dateTo: endDate.toISOString().split('T')[0],
|
// SyncSalesforce: result
|
||||||
// totalDays: totalDays,
|
// });
|
||||||
// leaveType,
|
// } catch (error) {
|
||||||
// comment: processedComment.processedHTML,
|
// console.error("Failed to sync with Salesforce:", error.message);
|
||||||
// // attachments: processedComment.extractedImages,
|
// console.log(error)
|
||||||
// status: "PENDING"
|
|
||||||
// },
|
// // Create a custom error to distinguish Salesforce errors
|
||||||
// // SyncSalesforce: result
|
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
// });
|
// salesforceError.isSalesforceError = true;
|
||||||
} catch (error) {
|
// throw salesforceError;
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
// }
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
salesforceError.isSalesforceError = true;
|
|
||||||
throw salesforceError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -225,133 +221,21 @@ async function va_leave_apply_function(req, res) {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Leave apply error:", error);
|
console.error("Leave apply error:", error);
|
||||||
|
|
||||||
if (error.name === 'JsonWebTokenError') {
|
if (error.name === 'JsonWebTokenError') {
|
||||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.name === 'TokenExpiredError') {
|
if (error.name === 'TokenExpiredError') {
|
||||||
return res.status(401).json({ success: false, message: "Token expired" });
|
return res.status(401).json({ success: false, message: "Token expired" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Internal Server Error",
|
message: "Internal Server Error",
|
||||||
error: error.message
|
error: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processHTMLComment(htmlComment) {
|
|
||||||
try {
|
|
||||||
let processedHTML = htmlComment;
|
|
||||||
const extractedImages = [];
|
|
||||||
let plainText = '';
|
|
||||||
|
|
||||||
// Extract plain text from HTML for Salesforce
|
|
||||||
plainText = extractPlainTextFromHTML(htmlComment);
|
|
||||||
|
|
||||||
// Process base64 images in HTML
|
|
||||||
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
|
|
||||||
processedHTML = imageProcessingResult.processedHTML;
|
|
||||||
extractedImages.push(...imageProcessingResult.extractedImages);
|
|
||||||
|
|
||||||
return {
|
|
||||||
processedHTML,
|
|
||||||
plainText,
|
|
||||||
extractedImages
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing HTML comment:", error);
|
|
||||||
// Return original content if processing fails
|
|
||||||
return {
|
|
||||||
processedHTML: htmlComment,
|
|
||||||
plainText: extractPlainTextFromHTML(htmlComment),
|
|
||||||
extractedImages: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract plain text from HTML
|
|
||||||
function extractPlainTextFromHTML(html) {
|
|
||||||
try {
|
|
||||||
// Remove HTML tags and decode HTML entities
|
|
||||||
let plainText = html
|
|
||||||
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
|
|
||||||
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// Decode common HTML entities
|
|
||||||
plainText = plainText
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(///g, '/');
|
|
||||||
|
|
||||||
return plainText;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error extracting plain text from HTML:", error);
|
|
||||||
return html.replace(/<[^>]*>/g, ' ').trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract and process base64 images from HTML
|
|
||||||
function extractAndProcessBase64Images(html) {
|
|
||||||
const extractedImages = [];
|
|
||||||
let processedHTML = html;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Regex to find base64 images in HTML (various formats)
|
|
||||||
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
let imageIndex = 0;
|
|
||||||
|
|
||||||
while ((match = base64Regex.exec(html)) !== null) {
|
|
||||||
const fullMatch = match[0];
|
|
||||||
const imageType = match[1]; // png, jpeg, gif, etc.
|
|
||||||
const base64Data = match[2];
|
|
||||||
|
|
||||||
// Create a unique filename
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`;
|
|
||||||
|
|
||||||
// Calculate approximate file size
|
|
||||||
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
|
|
||||||
|
|
||||||
// Store image information
|
|
||||||
extractedImages.push({
|
|
||||||
filename: filename,
|
|
||||||
mimeType: `image/${imageType}`,
|
|
||||||
base64Data: base64Data,
|
|
||||||
size: size,
|
|
||||||
originalHTML: fullMatch, // Store original img tag for reference
|
|
||||||
index: imageIndex
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace base64 with a reference in HTML
|
|
||||||
const replacementHTML = `<img src="attachment://${filename}" alt="Leave evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="leave-image">`;
|
|
||||||
|
|
||||||
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
|
|
||||||
|
|
||||||
imageIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
processedHTML,
|
|
||||||
extractedImages
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing base64 images:", error);
|
|
||||||
return {
|
|
||||||
processedHTML: html,
|
|
||||||
extractedImages: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { va_leave_apply_function };
|
export { va_leave_apply_function };
|
||||||
|
|
@ -5,10 +5,10 @@ import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../mod
|
||||||
function getDayRange(date = new Date()) {
|
function getDayRange(date = new Date()) {
|
||||||
const start = new Date(date);
|
const start = new Date(date);
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const end = new Date(date);
|
const end = new Date(date);
|
||||||
end.setHours(23, 59, 59, 999);
|
end.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
return { start, end };
|
return { start, end };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ function formatHoursToReadable(hours) {
|
||||||
const wholeHours = Math.floor(hours);
|
const wholeHours = Math.floor(hours);
|
||||||
const decimalMinutes = (hours - wholeHours) * 60;
|
const decimalMinutes = (hours - wholeHours) * 60;
|
||||||
const wholeMinutes = Math.round(decimalMinutes);
|
const wholeMinutes = Math.round(decimalMinutes);
|
||||||
|
|
||||||
if (wholeHours > 0 && wholeMinutes > 0) {
|
if (wholeHours > 0 && wholeMinutes > 0) {
|
||||||
return `${wholeHours}h ${wholeMinutes}m`;
|
return `${wholeHours}h ${wholeMinutes}m`;
|
||||||
} else if (wholeHours > 0) {
|
} else if (wholeHours > 0) {
|
||||||
|
|
@ -26,274 +26,245 @@ function formatHoursToReadable(hours) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Helper function to check daily hours and create logout log if needed
|
// Helper function to check daily hours and create logout log if needed
|
||||||
// async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
|
async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
|
||||||
// try {
|
try {
|
||||||
// const today = new Date();
|
const today = new Date();
|
||||||
// const { start, end } = getDayRange(today);
|
const { start, end } = getDayRange(today);
|
||||||
|
|
||||||
|
// Get today's work entries for this VA and project
|
||||||
|
// Exclude the current session if it's already in the database
|
||||||
|
const query = {
|
||||||
|
vaId,
|
||||||
|
projectId,
|
||||||
|
from: { $gte: start, $lte: end }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentSessionId) {
|
||||||
|
query._id = { $ne: currentSessionId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const todayWorkEntries = await WorkTrackModel.find(query);
|
||||||
|
|
||||||
|
// Calculate total hours worked today
|
||||||
|
let totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
||||||
|
return total + (entry.totalTime || 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Add the additional time from current session if provided
|
||||||
|
totalTodayMs += additionalTimeMs;
|
||||||
|
|
||||||
|
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
// Check if daily dedicated hours are completed
|
||||||
|
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
|
||||||
|
// Create automatic logout log
|
||||||
|
const logoutLog = await LoggedModel.create({
|
||||||
|
loggedType: "LOGOUT",
|
||||||
|
vaId,
|
||||||
|
vaEmail,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
// // Get today's work entries for this VA and project
|
try {
|
||||||
// // Exclude the current session if it's already in the database
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||||
// const query = {
|
const sfDataSync = {
|
||||||
// vaId,
|
"_id": logoutLog._id,
|
||||||
// projectId,
|
"loggedType": "LOGOUT",
|
||||||
// from: { $gte: start, $lte: end }
|
"vaId": vaId,
|
||||||
// };
|
"vaEmail": vaEmail,
|
||||||
|
"projectId": projectId,
|
||||||
|
"time": logoutLog.time,
|
||||||
|
}
|
||||||
|
|
||||||
// if (currentSessionId) {
|
const response = await fetch(salesforceUrl, {
|
||||||
// query._id = { $ne: currentSessionId };
|
method: 'POST',
|
||||||
// }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
// Add any required headers for Salesforce authentication
|
||||||
|
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
|
},
|
||||||
|
body: JSON.stringify(sfDataSync)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
// const todayWorkEntries = await WorkTrackModel.find(query);
|
const result = await response.json();
|
||||||
|
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
// // Calculate total hours worked today
|
|
||||||
// let totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
return {
|
||||||
// return total + (entry.totalTime || 0);
|
shouldLogout: true,
|
||||||
// }, 0);
|
totalTodayHours: totalTodayHours,
|
||||||
|
dedicatedHours: project.dedicatedHours,
|
||||||
// // Add the additional time from current session if provided
|
logoutLogId: logoutLog._id,
|
||||||
// totalTodayMs += additionalTimeMs;
|
SyncSalesforce: result
|
||||||
|
};
|
||||||
// const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
} catch (error) {
|
||||||
|
console.error("Failed to sync with Salesforce:", error.message);
|
||||||
// // Check if daily dedicated hours are completed
|
console.log(error)
|
||||||
// if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
|
|
||||||
// // Create automatic logout log
|
// Create a custom error to distinguish Salesforce errors
|
||||||
// const logoutLog = await LoggedModel.create({
|
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
// loggedType: "LOGOUT",
|
salesforceError.isSalesforceError = true;
|
||||||
// vaId,
|
throw salesforceError;
|
||||||
// vaEmail,
|
}
|
||||||
// projectId
|
}
|
||||||
// });
|
|
||||||
|
return {
|
||||||
// try {
|
shouldLogout: false,
|
||||||
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
totalTodayHours: totalTodayHours,
|
||||||
// const sfDataSync = {
|
dedicatedHours: project.dedicatedHours
|
||||||
// "_id": logoutLog._id,
|
};
|
||||||
// "loggedType": "LOGOUT",
|
} catch (error) {
|
||||||
// "vaId": vaId,
|
console.error("Error in checkDailyHoursAndLogout:", error);
|
||||||
// "vaEmail": vaEmail,
|
return { shouldLogout: false, error: error.message };
|
||||||
// "projectId": projectId,
|
}
|
||||||
// "time": logoutLog.time,
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// const response = await fetch(salesforceUrl, {
|
|
||||||
// method: 'POST',
|
|
||||||
// headers: {
|
|
||||||
// 'Content-Type': 'application/json',
|
|
||||||
// // Add any required headers for Salesforce authentication
|
|
||||||
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
// },
|
|
||||||
// body: JSON.stringify(sfDataSync)
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!response.ok) {
|
|
||||||
// const errorText = await response.text();
|
|
||||||
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const result = await response.json();
|
|
||||||
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// shouldLogout: true,
|
|
||||||
// totalTodayHours: totalTodayHours,
|
|
||||||
// dedicatedHours: project.dedicatedHours,
|
|
||||||
// logoutLogId: logoutLog._id,
|
|
||||||
// SyncSalesforce: result
|
|
||||||
// };
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Failed to sync with Salesforce:", error.message);
|
|
||||||
// console.log(error)
|
|
||||||
|
|
||||||
// // Create a custom error to distinguish Salesforce errors
|
|
||||||
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
// salesforceError.isSalesforceError = true;
|
|
||||||
// throw salesforceError;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// shouldLogout: false,
|
|
||||||
// totalTodayHours: totalTodayHours,
|
|
||||||
// dedicatedHours: project.dedicatedHours
|
|
||||||
// };
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Error in checkDailyHoursAndLogout:", error);
|
|
||||||
// return { shouldLogout: false, error: error.message };
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// Getting logged hours in a specific project of VA
|
// Getting logged hours in a specific project of VA
|
||||||
async function va_log_track_work_callback(req, res) {
|
async function va_log_track_work_callback(req, res) {
|
||||||
try {
|
try {
|
||||||
// const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
// if (!accessToken) {
|
if (!accessToken) {
|
||||||
// return res.status(401).json({ success: false, message: "Missing token" });
|
return res.status(401).json({ success: false, message: "Missing token" });
|
||||||
// }
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
const payload = req.body || {};
|
|
||||||
|
|
||||||
let { vaId, projectId, logId, logs } = payload;
|
|
||||||
|
|
||||||
if (!projectId || !logId || !vaId || !Array.isArray(logs)) {
|
|
||||||
return res.status(400).json({ success: false, message: "Missing Props" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaUser = await VAModel.findById(vaId);
|
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||||
if (!vaUser) return res.status(400).json({ success: false, message: "VA not found" });
|
|
||||||
|
|
||||||
const project = await ProjectModel.findById(projectId);
|
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;
|
||||||
|
const payload = req.body || {};
|
||||||
|
const { projectId, logId, title, appName } = payload;
|
||||||
|
|
||||||
|
const project = await ProjectModel.findById(projectId);
|
||||||
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
|
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
|
||||||
|
|
||||||
const logEntry = await LoggedModel.findById(logId);
|
const logEntry = await LoggedModel.findById(logId);
|
||||||
if (!logEntry) return res.status(400).json({ success: false, message: "Log entry not found" });
|
if (!logEntry) return res.status(400).json({ success: false, message: "Log entry not found" });
|
||||||
|
|
||||||
if (logs.length === 0) {
|
// Check last log entry for this VA and project
|
||||||
return res.status(400).json({ success: false, message: "No logs to track" });
|
const lastLog = await LoggedModel.findOne({ vaId, projectId })
|
||||||
|
.sort({ time: -1 }) // get the most recent log
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
// If the last log is a LOGOUT, prevent new work tracking
|
||||||
|
if (lastLog.loggedType === "LOGOUT") {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "User has already logged out from this project",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logs[0].from = logEntry.time; // Ensure the first log's 'from' matches the main log time
|
if (!projectId || !logId || !title || !appName) {
|
||||||
|
return res.status(400).json({ success: false, message: "Missing Props" });
|
||||||
const firstFromLogTime = new Date(logs[0].from);
|
}
|
||||||
const firstToLogTime = new Date(logs[0].to);
|
|
||||||
const firstTotalLogTime = Math.abs(firstToLogTime - firstFromLogTime);
|
|
||||||
logs[0].totalTime = firstTotalLogTime;
|
|
||||||
|
|
||||||
|
|
||||||
// // Check last log entry for this VA and project
|
|
||||||
// const lastLog = await LoggedModel.findOne({ vaId, projectId })
|
|
||||||
// .sort({ time: -1 }) // get the most recent log
|
|
||||||
// .lean();
|
|
||||||
|
|
||||||
// // If the last log is a LOGOUT, prevent new work tracking
|
|
||||||
// if (lastLog.loggedType === "LOGOUT") {
|
|
||||||
// return res.status(400).json({
|
|
||||||
// success: false,
|
|
||||||
// message: "User has already logged out from this project",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Handle POST → create entry
|
// Handle POST → create entry
|
||||||
// if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
// const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
|
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
|
||||||
// const createdTracks = await Promise.all(
|
|
||||||
// logs.map(async (log) => {
|
if (dailyCheck.shouldLogout) {
|
||||||
// // Calculate total time from timestamps
|
return res.status(400).json({
|
||||||
// const fromTime = new Date(log.from);
|
success: false,
|
||||||
// const toTime = new Date(log.to);
|
message: `Cannot start work - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`,
|
||||||
// const totalTime = Math.abs(toTime - fromTime);
|
autoLogout: true
|
||||||
// const createdTrack = await WorkTrackModel.create({
|
});
|
||||||
// vaId,
|
}
|
||||||
// projectId,
|
|
||||||
// logId,
|
|
||||||
// title: log.title,
|
|
||||||
// appName: log.appName,
|
|
||||||
// url: log.url,
|
|
||||||
// from: log.from,
|
|
||||||
// to: log.to,
|
|
||||||
// totalTime: totalTime,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return createdTrack;
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
const fromLogTime = new Date(logs[0].from);
|
|
||||||
const toLogTime = new Date(logs[logs.length -1].to);
|
|
||||||
const totalLogTime = Math.abs(toLogTime - fromLogTime);
|
|
||||||
|
|
||||||
const createdTrack = await WorkTrackModel.create({
|
const createdTrack = await WorkTrackModel.create({
|
||||||
vaId,
|
vaId,
|
||||||
projectId,
|
projectId,
|
||||||
logId,
|
logId,
|
||||||
from: logs[0].from,
|
title,
|
||||||
to: logs[logs.length -1].to,
|
appName,
|
||||||
totalTime: totalLogTime,
|
from: Date.now(),
|
||||||
logs: logs
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Work tracking created",
|
message: "Work tracking started",
|
||||||
data: createdTrack
|
data: createdTrack,
|
||||||
|
dailyHours: {
|
||||||
|
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
|
||||||
|
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
|
||||||
|
remaining: formatHoursToReadable(dailyCheck.dedicatedHours - dailyCheck.totalTodayHours)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Handle PATCH → update entry (add 'to' and calculate total time)
|
// Handle PATCH → update entry (add 'to' and calculate total time)
|
||||||
// if (req.method === "PATCH") {
|
if (req.method === "PATCH") {
|
||||||
// const { id } = payload; // expecting the workTrack record ID to update
|
const { id } = payload; // expecting the workTrack record ID to update
|
||||||
// if (!id) {
|
if (!id) {
|
||||||
// return res.status(400).json({ success: false, message: "Missing workTrack ID" });
|
return res.status(400).json({ success: false, message: "Missing workTrack ID" });
|
||||||
// }
|
}
|
||||||
|
|
||||||
// const workEntry = await WorkTrackModel.findById(id);
|
const workEntry = await WorkTrackModel.findById(id);
|
||||||
// if (!workEntry) {
|
if (!workEntry) {
|
||||||
// return res.status(404).json({ success: false, message: "Work entry not found" });
|
return res.status(404).json({ success: false, message: "Work entry not found" });
|
||||||
// }
|
}
|
||||||
|
|
||||||
// const toTime = new Date();
|
const toTime = new Date();
|
||||||
// const fromTime = new Date(workEntry.from);
|
const fromTime = new Date(workEntry.from);
|
||||||
// const totalTimeMs = toTime - fromTime;
|
const totalTimeMs = toTime - fromTime;
|
||||||
|
|
||||||
// // Optional: store totalTime in minutes or hours instead of raw ms
|
// Optional: store totalTime in minutes or hours instead of raw ms
|
||||||
// const hours = Math.floor(totalTimeMs / (1000 * 60 * 60));
|
const hours = Math.floor(totalTimeMs / (1000 * 60 * 60));
|
||||||
// const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60));
|
const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
// const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000);
|
const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
// const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`;
|
const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`;
|
||||||
|
|
||||||
// // Check if daily dedicated hours are completed after this work session
|
// Check if daily dedicated hours are completed after this work session
|
||||||
// const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
|
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
|
||||||
|
|
||||||
// workEntry.to = toTime;
|
workEntry.to = toTime;
|
||||||
// workEntry.totalTime = totalTimeMs; // store as number of minutes
|
workEntry.totalTime = totalTimeMs; // store as number of minutes
|
||||||
// await workEntry.save();
|
await workEntry.save();
|
||||||
|
|
||||||
// const response = {
|
const response = {
|
||||||
// success: true,
|
success: true,
|
||||||
// message: "Work tracking stopped",
|
message: "Work tracking stopped",
|
||||||
// data: {
|
data: {
|
||||||
// id: workEntry._id,
|
id: workEntry._id,
|
||||||
// from: workEntry.from,
|
from: workEntry.from,
|
||||||
// to: workEntry.to,
|
to: workEntry.to,
|
||||||
// formattedTotalTime,
|
formattedTotalTime,
|
||||||
// totalTimeMs,
|
totalTimeMs,
|
||||||
// sessionHours: totalTimeMs / (1000 * 60 * 60)
|
sessionHours: totalTimeMs / (1000 * 60 * 60)
|
||||||
// },
|
},
|
||||||
// dailyHours: {
|
dailyHours: {
|
||||||
// todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
|
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
|
||||||
// dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
|
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
|
||||||
// remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
|
remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
|
||||||
// }
|
}
|
||||||
// };
|
};
|
||||||
|
|
||||||
// // If auto-logout was triggered, add that info to response
|
// If auto-logout was triggered, add that info to response
|
||||||
// if (dailyCheck.shouldLogout) {
|
if (dailyCheck.shouldLogout) {
|
||||||
// response.autoLogout = true;
|
response.autoLogout = true;
|
||||||
// response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
|
response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
|
||||||
// response.logoutLogId = dailyCheck.logoutLogId;
|
response.logoutLogId = dailyCheck.logoutLogId;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// return res.status(200).json(response);
|
return res.status(200).json(response);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Fallback for unsupported methods
|
// Fallback for unsupported methods
|
||||||
// return res.status(405).json({ success: false, message: "Method not allowed" });
|
return res.status(405).json({ success: false, message: "Method not allowed" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(500).send("Internal Server Error");
|
return res.status(500).send("Internal Server Error");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import { VAModel } from "../../../models/vaModel.js";
|
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
async function va_payslip_data_function(req, res) {
|
|
||||||
try {
|
|
||||||
const vaId = req.params.vaId;
|
|
||||||
|
|
||||||
const vaUser = await VAModel.findById(vaId);
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip/${vaId}`;
|
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: result.data
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
salesforceError.isSalesforceError = true;
|
|
||||||
throw salesforceError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { va_payslip_data_function };
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
async function va_payroll_schedule_function(req, res) {
|
|
||||||
try {
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/payroll-schedule`;
|
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
// Get current date
|
|
||||||
const currentDate = new Date();
|
|
||||||
|
|
||||||
// Sort all data by ScheduleDate in ascending order
|
|
||||||
const sortedData = [...result.data].sort((a, b) => {
|
|
||||||
return new Date(a.ScheduleDate) - new Date(b.ScheduleDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find the index of current schedule (where current date falls between periodStart and periodEnd)
|
|
||||||
let currentIndex = -1;
|
|
||||||
for (let i = 0; i < sortedData.length; i++) {
|
|
||||||
const item = sortedData[i];
|
|
||||||
const periodStart = new Date(item.periodStart);
|
|
||||||
const periodEnd = new Date(item.periodEnd);
|
|
||||||
|
|
||||||
if (currentDate >= periodStart && currentDate <= periodEnd) {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no current schedule found, use the schedule that just passed or is upcoming
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
// Find the first schedule where periodEnd is in future
|
|
||||||
currentIndex = sortedData.findIndex(item => new Date(item.periodEnd) >= currentDate);
|
|
||||||
|
|
||||||
// If all are in past, use the last one
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
currentIndex = sortedData.length - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the start index (2nd last schedule from current)
|
|
||||||
const startIndex = Math.max(0, currentIndex - 2);
|
|
||||||
|
|
||||||
// Get the filtered data from 2nd last to all future schedules
|
|
||||||
const filteredData = sortedData.slice(startIndex);
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: filteredData
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
salesforceError.isSalesforceError = true;
|
|
||||||
throw salesforceError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { va_payroll_schedule_function };
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
|
|
||||||
|
|
||||||
// Delete VA and all related data
|
|
||||||
async function va_remove_function(req, res) {
|
|
||||||
try {
|
|
||||||
const vaId = req.params.id;
|
|
||||||
|
|
||||||
// Validate VA ID
|
|
||||||
if (!vaId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "VA ID is required"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the VA to get clientIds and projectIds before deletion
|
|
||||||
const vaUser = await VAModel.findById(vaId);
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: "VA not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientIds = vaUser.clientIds || [];
|
|
||||||
const projectIds = vaUser.projectIds || [];
|
|
||||||
const vaEmail = vaUser.userDetails?.email;
|
|
||||||
|
|
||||||
console.log(`Deleting VA ${vaId} - Affected: ${clientIds.length} clients, ${projectIds.length} projects`);
|
|
||||||
|
|
||||||
// 1. Remove VA from all Clients
|
|
||||||
let clientsUpdated = 0;
|
|
||||||
if (clientIds.length > 0) {
|
|
||||||
const clientResult = await ClientModel.updateMany(
|
|
||||||
{ _id: { $in: clientIds } },
|
|
||||||
{ $pull: { vaIds: vaId } }
|
|
||||||
);
|
|
||||||
clientsUpdated = clientResult.modifiedCount;
|
|
||||||
console.log(`Removed VA from ${clientsUpdated} clients`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Remove VA from all Projects
|
|
||||||
let projectsUpdated = 0;
|
|
||||||
if (projectIds.length > 0) {
|
|
||||||
const projectResult = await ProjectModel.updateMany(
|
|
||||||
{ _id: { $in: projectIds } },
|
|
||||||
{ $pull: { "vaAssigned.vaId": vaId } }
|
|
||||||
);
|
|
||||||
projectsUpdated = projectResult.modifiedCount;
|
|
||||||
console.log(`Removed VA from ${projectsUpdated} projects`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Delete all WorkTrack records for this VA
|
|
||||||
const workTrackResult = await WorkTrackModel.deleteMany({ vaId: vaId });
|
|
||||||
console.log(`Deleted ${workTrackResult.deletedCount} work track records`);
|
|
||||||
|
|
||||||
// 4. Delete all Logged records for this VA
|
|
||||||
const loggedResult = await LoggedModel.deleteMany({ vaId: vaId });
|
|
||||||
console.log(`Deleted ${loggedResult.deletedCount} log records`);
|
|
||||||
|
|
||||||
// 5. Finally delete the VA
|
|
||||||
await VAModel.findByIdAndDelete(vaId);
|
|
||||||
|
|
||||||
console.log(`Successfully deleted VA: ${vaId} and all related data`);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Virtual Assistant and all related data deleted successfully',
|
|
||||||
data: {
|
|
||||||
vaId: vaId,
|
|
||||||
vaName: `${vaUser.userDetails?.firstName || ''} ${vaUser.userDetails?.lastName || ''}`.trim() || 'Unknown VA',
|
|
||||||
vaEmail: vaEmail,
|
|
||||||
clientsUpdated: clientsUpdated,
|
|
||||||
projectsUpdated: projectsUpdated,
|
|
||||||
workTracksDeleted: workTrackResult.deletedCount,
|
|
||||||
logsDeleted: loggedResult.deletedCount,
|
|
||||||
deletedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting VA:', error);
|
|
||||||
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Failed to delete Virtual Assistant',
|
|
||||||
error: 'Internal server error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { va_remove_function };
|
|
||||||
|
|
@ -5,96 +5,52 @@ import { VAModel } from "../../../models/vaModel.js";
|
||||||
// Register a new VA
|
// Register a new VA
|
||||||
async function va_profile_update_callback(req, res) {
|
async function va_profile_update_callback(req, res) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
let vaUser;
|
if (!accessToken) {
|
||||||
let authSource = 'regular';
|
return res.status(401).json({ success: false, message: "Missing access token" });
|
||||||
if(sf_token) {
|
}
|
||||||
try {
|
|
||||||
const sfDecoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
let decoded;
|
||||||
if (!sfDecoded?.email && !sfDecoded?.portalId) {
|
try {
|
||||||
return res.status(400).json({ success: false, message: "Invalid Salesforce token payload" });
|
decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||||
}
|
} catch (err) {
|
||||||
|
return res.status(401).json({ success: false, message: "Invalid or expired token" });
|
||||||
// Find user by email or VA ID from Salesforce token
|
}
|
||||||
if (sfDecoded.portalId) {
|
|
||||||
vaUser = await VAModel.findById(sfDecoded.portalId);
|
if (!decoded?.email) {
|
||||||
} else if (sfDecoded.email) {
|
return res.status(400).json({ success: false, message: "Invalid token payload" });
|
||||||
vaUser = await VAModel.findOne({ "userDetails.email": sfDecoded.email });
|
}
|
||||||
}
|
|
||||||
|
const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
|
||||||
if (!vaUser) {
|
if (!vaUser) {
|
||||||
return res.status(404).json({ success: false, message: "VA not found with Salesforce token" });
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
}
|
|
||||||
|
|
||||||
authSource = 'salesforce';
|
|
||||||
|
|
||||||
} catch (sfError) {
|
|
||||||
return res.status(401).json({ success: false, message: "Invalid or expired Salesforce token" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ success: false, message: "Missing access token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
|
||||||
|
|
||||||
if (!decoded?.email) {
|
|
||||||
return res.status(400).json({ success: false, message: "Invalid token payload" });
|
|
||||||
}
|
|
||||||
|
|
||||||
vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
authSource = 'regular';
|
|
||||||
|
|
||||||
} catch (tokenError) {
|
|
||||||
return res.status(401).json({ success: false, message: "Invalid or expired access token" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const payload = req.body || {};
|
const payload = req.body || {};
|
||||||
const { userDetails, status } = payload;
|
const { userDetails } = payload;
|
||||||
|
|
||||||
if (!userDetails) {
|
if (!userDetails) {
|
||||||
return res.status(400).json({ success: false, message: "Missing userDetails" });
|
return res.status(400).json({ success: false, message: "Missing userDetails" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedFields = [
|
// Allowed fields to update
|
||||||
"firstName", "lastName", "dob", "age", "address",
|
// const allowedFields = [
|
||||||
"city", "state", "country", "phone", "skype",
|
// "firstName", "lastName", "dob", "age", "address",
|
||||||
"paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail"
|
// "bankName", "accountHolderName", "accountNumber", "routingNumber"
|
||||||
];
|
// ];
|
||||||
|
|
||||||
let hasUpdates = false;
|
// // Update allowed fields only
|
||||||
const updatedFields = [];
|
// allowedFields.forEach(field => {
|
||||||
|
// if (userDetails[field] !== undefined) {
|
||||||
// Update allowed fields if provided
|
// vaUser.userDetails[field] = userDetails[field];
|
||||||
allowedFields.forEach(field => {
|
// }
|
||||||
if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') {
|
// });
|
||||||
vaUser.userDetails[field] = userDetails[field];
|
|
||||||
hasUpdates = true;
|
|
||||||
updatedFields.push(field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ---------------------- PASSWORD UPDATE ---------------------- */
|
/* ---------------------- PASSWORD UPDATE ---------------------- */
|
||||||
let passwordUpdated = false;
|
|
||||||
if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) {
|
if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) {
|
||||||
const { oldPassword, password, cPassword } = userDetails;
|
const { oldPassword, password, cPassword } = userDetails;
|
||||||
|
|
||||||
if (authSource === 'salesforce') {
|
|
||||||
return res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
message: "Password changes require regular authentication",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oldPassword || !password || !cPassword) {
|
if (!oldPassword || !password || !cPassword) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -122,21 +78,11 @@ async function va_profile_update_callback(req, res) {
|
||||||
const saltRounds = 10;
|
const saltRounds = 10;
|
||||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||||
vaUser.userDetails.password = hashedPassword;
|
vaUser.userDetails.password = hashedPassword;
|
||||||
hasUpdates = true;
|
} else {
|
||||||
passwordUpdated = true;
|
|
||||||
updatedFields.push('password');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasUpdates) {
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "No valid fields to update",
|
message: "Data not found",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
vaUser.status = status;
|
|
||||||
updatedFields.push('status');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await vaUser.save();
|
await vaUser.save();
|
||||||
|
|
@ -149,7 +95,6 @@ async function va_profile_update_callback(req, res) {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Profile updated successfully",
|
message: "Profile updated successfully",
|
||||||
data: updatedUser,
|
data: updatedUser,
|
||||||
updatedFields: updatedFields,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { VAModel } from "../../../models/vaModel.js";
|
import { VAModel } from "../../../models/vaModel.js";
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
// Register a new VA
|
// Register a new VA
|
||||||
async function va_register(req, res) {
|
async function va_register(req, res) {
|
||||||
|
|
@ -37,7 +36,6 @@ async function va_register(req, res) {
|
||||||
paymentMethod: userDetails.paymentMethod || null,
|
paymentMethod: userDetails.paymentMethod || null,
|
||||||
payoneerAccount: userDetails.payoneerAccount || null,
|
payoneerAccount: userDetails.payoneerAccount || null,
|
||||||
veemAccount: userDetails.veemAccount || null,
|
veemAccount: userDetails.veemAccount || null,
|
||||||
wiseEmail: userDetails.wiseEmail || null,
|
|
||||||
phone: userDetails.phone || null,
|
phone: userDetails.phone || null,
|
||||||
skype: userDetails.skype || null,
|
skype: userDetails.skype || null,
|
||||||
},
|
},
|
||||||
|
|
@ -54,7 +52,7 @@ async function va_register(req, res) {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
|
||||||
delete created.userDetails._id;
|
delete created.userDetails._id;
|
||||||
created.userDetails._id = created._id; // Include VA's MongoDB ID
|
created.userDetails._id = created._id; // Include VA's MongoDB ID
|
||||||
const response = await authFetch(salesforceUrl, {
|
const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -75,7 +73,7 @@ async function va_register(req, res) {
|
||||||
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to sync with Salesforce:", error.message);
|
console.error("❌ Failed to sync with Salesforce:", error.message);
|
||||||
await VAModel.findOneAndDelete({_id: created.userDetails._id});
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
// Create a custom error to distinguish Salesforce errors
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
salesforceError.isSalesforceError = true;
|
salesforceError.isSalesforceError = true;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
import mongoose from "mongoose";
|
import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js";
|
||||||
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
|
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
|
||||||
|
|
||||||
// Helper function to get start and end of day in UTC
|
// Helper function to get start and end of day
|
||||||
function getDayRange(date = new Date()) {
|
function getDayRange(date = new Date()) {
|
||||||
const start = new Date(date);
|
const start = new Date(date);
|
||||||
start.setUTCHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const end = new Date(date);
|
const end = new Date(date);
|
||||||
end.setUTCHours(23, 59, 59, 999);
|
end.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
return { start, end };
|
return { start, end };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,7 +15,7 @@ function formatHoursToReadable(hours) {
|
||||||
const wholeHours = Math.floor(hours);
|
const wholeHours = Math.floor(hours);
|
||||||
const decimalMinutes = (hours - wholeHours) * 60;
|
const decimalMinutes = (hours - wholeHours) * 60;
|
||||||
const wholeMinutes = Math.round(decimalMinutes);
|
const wholeMinutes = Math.round(decimalMinutes);
|
||||||
|
|
||||||
if (wholeHours > 0 && wholeMinutes > 0) {
|
if (wholeHours > 0 && wholeMinutes > 0) {
|
||||||
return `${wholeHours}h ${wholeMinutes}m`;
|
return `${wholeHours}h ${wholeMinutes}m`;
|
||||||
} else if (wholeHours > 0) {
|
} else if (wholeHours > 0) {
|
||||||
|
|
@ -28,131 +25,11 @@ function formatHoursToReadable(hours) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to parse ISO time string
|
|
||||||
function parseISOTimeString(timeString) {
|
|
||||||
if (!timeString) return new Date();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse ISO string directly
|
|
||||||
const parsedDate = new Date(timeString);
|
|
||||||
|
|
||||||
// Check if the date is valid
|
|
||||||
if (isNaN(parsedDate.getTime())) {
|
|
||||||
throw new Error('Invalid date');
|
|
||||||
}
|
|
||||||
parsedDate.setMilliseconds(0);
|
|
||||||
|
|
||||||
console.log("pasdes date" + parsedDate)
|
|
||||||
return parsedDate;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing ISO time string:', error);
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if time difference is less than threshold
|
|
||||||
function isTimeWithinThreshold(receivedTime, currentTime, thresholdSeconds = 10) {
|
|
||||||
const diffMs = Math.abs(currentTime - receivedTime);
|
|
||||||
const diffSeconds = diffMs / 1000;
|
|
||||||
console.log(`Time comparison - Received: ${receivedTime.toISOString()}, Current: ${currentTime.toISOString()}, Diff: ${diffSeconds}s`);
|
|
||||||
return diffSeconds <= thresholdSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get current UTC time
|
|
||||||
function getCurrentUTCTime() {
|
|
||||||
const now = new Date();
|
|
||||||
now.setMilliseconds(0);
|
|
||||||
return now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// function parseTimeToMinutes(timeString) {
|
|
||||||
// if (!timeString) return null;
|
|
||||||
|
|
||||||
// const [hours, minutes] = timeString.split(':').map(Number);
|
|
||||||
// return hours * 60 + (minutes || 0);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function parseDelayToMinutes(delayString) {
|
|
||||||
// if (!delayString) return 0;
|
|
||||||
|
|
||||||
// if (delayString.includes('h')) {
|
|
||||||
// const hours = parseInt(delayString.replace('h', '').trim());
|
|
||||||
// return hours * 60;
|
|
||||||
// } else if (delayString.includes('m')) {
|
|
||||||
// return parseInt(delayString.replace('m', '').trim());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return parseInt(delayString) || 0; // Default to minutes if no unit specified
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function checkScheduleCompliance(currentTime, project, vaStatus, type) {
|
|
||||||
// const warnings = [];
|
|
||||||
|
|
||||||
// // If project doesn't have schedule or VA is PARTTIME with flexible allowed, no schedule restrictions
|
|
||||||
// if (!project.vaWorkingScheduleStart || !project.vaWorkingScheduleEnd) {
|
|
||||||
// return { isCompliant: true, warnings };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const currentDate = new Date(currentTime);
|
|
||||||
// const currentMinutes = currentDate.getUTCHours() * 60 + currentDate.getUTCMinutes();
|
|
||||||
|
|
||||||
// const scheduleStartMinutes = parseTimeToMinutes(project.vaWorkingScheduleStart);
|
|
||||||
// const scheduleEndMinutes = parseTimeToMinutes(project.vaWorkingScheduleEnd);
|
|
||||||
// const allowedStartDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedStart);
|
|
||||||
// const allowedEndDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedEnd);
|
|
||||||
|
|
||||||
// // Calculate allowed time ranges
|
|
||||||
// const allowedStartMinutes = scheduleStartMinutes - allowedStartDelay;
|
|
||||||
// const allowedEndMinutes = scheduleEndMinutes + allowedEndDelay;
|
|
||||||
|
|
||||||
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
|
|
||||||
|
|
||||||
// // Check for PARTTIME with flexible schedule
|
|
||||||
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowed) {
|
|
||||||
// return { isCompliant: true, warnings };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // For all other cases, check schedule compliance
|
|
||||||
// if (type === "LOGIN") {
|
|
||||||
// // Check if login is too early
|
|
||||||
// if (currentMinutes < allowedStartMinutes) {
|
|
||||||
// warnings.push("early_login");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Check if login is after shift end
|
|
||||||
// if (currentMinutes > scheduleEndMinutes) {
|
|
||||||
// warnings.push("working_after_shift_end");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Check if login is within allowed range
|
|
||||||
// // const isWithinAllowedRange = currentMinutes >= allowedStartMinutes && currentMinutes <= allowedEndMinutes;
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// isCompliant: true, // Allow login regardless of time
|
|
||||||
// warnings
|
|
||||||
// };
|
|
||||||
|
|
||||||
// } else if (type === "LOGOUT") {
|
|
||||||
// // Check if logout is too late
|
|
||||||
// if (currentMinutes > allowedEndMinutes) {
|
|
||||||
// warnings.push("late_logout");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // For logout, we're generally more permissive - allow logout even if late
|
|
||||||
// return {
|
|
||||||
// isCompliant: true, // Allow logout regardless of time
|
|
||||||
// warnings
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return { isCompliant: true, warnings };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function to close open work tracks and create logout
|
// function to close open work tracks and create logout
|
||||||
async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
async function closePreviousSession(vaId, lastLog) {
|
||||||
try {
|
try {
|
||||||
const currentTime = logTime;
|
const currentTime = new Date();
|
||||||
|
|
||||||
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
|
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
|
||||||
const openWorkTracks = await WorkTrackModel.find({
|
const openWorkTracks = await WorkTrackModel.find({
|
||||||
vaId,
|
vaId,
|
||||||
|
|
@ -163,22 +40,22 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
||||||
{ to: null }
|
{ to: null }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
|
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
|
||||||
|
|
||||||
// Close all open work tracks
|
// Close all open work tracks
|
||||||
for (const workTrack of openWorkTracks) {
|
for (const workTrack of openWorkTracks) {
|
||||||
const fromTime = new Date(workTrack.from);
|
const fromTime = new Date(workTrack.from);
|
||||||
const totalTimeMs = currentTime - fromTime;
|
const totalTimeMs = currentTime - fromTime;
|
||||||
|
|
||||||
// Update the work track with end time and total time
|
// Update the work track with end time and total time
|
||||||
workTrack.to = currentTime;
|
workTrack.to = currentTime;
|
||||||
workTrack.totalTime = totalTimeMs;
|
workTrack.totalTime = totalTimeMs;
|
||||||
await workTrack.save();
|
await workTrack.save();
|
||||||
|
|
||||||
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
|
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a logout log entry
|
// Create a logout log entry
|
||||||
const logoutLog = await LoggedModel.create({
|
const logoutLog = await LoggedModel.create({
|
||||||
loggedType: "LOGOUT",
|
loggedType: "LOGOUT",
|
||||||
|
|
@ -187,20 +64,20 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
||||||
projectId: lastLog.projectId,
|
projectId: lastLog.projectId,
|
||||||
time: currentTime // Use same timestamp for consistent logging
|
time: currentTime // Use same timestamp for consistent logging
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(`Created logout log: ${logoutLog._id}`);
|
// console.log(`Created logout log: ${logoutLog._id}`);
|
||||||
try {
|
try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||||
const sfDataSync = {
|
const sfDataSync = {
|
||||||
"_id": logoutLog._id,
|
"_id": logoutLog._id,
|
||||||
"loggedType": "LOGOUT",
|
"loggedType": "LOGOUT",
|
||||||
"vaId": vaId,
|
"vaId": vaId,
|
||||||
"vaEmail": lastLog.vaEmail,
|
"vaEmail": lastLog.vaEmail,
|
||||||
"projectId": lastLog.projectId,
|
"projectId": lastLog.projectId,
|
||||||
"time": logoutLog.time,
|
"time": logoutLog.time,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -209,7 +86,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
||||||
},
|
},
|
||||||
body: JSON.stringify(sfDataSync)
|
body: JSON.stringify(sfDataSync)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
|
|
@ -217,7 +94,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
closedWorkTracks: openWorkTracks.length,
|
closedWorkTracks: openWorkTracks.length,
|
||||||
logoutLogId: logoutLog._id,
|
logoutLogId: logoutLog._id,
|
||||||
|
|
@ -226,13 +103,13 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
console.error("Failed to sync with Salesforce:", error.message);
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
// Create a custom error to distinguish Salesforce errors
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
salesforceError.isSalesforceError = true;
|
salesforceError.isSalesforceError = true;
|
||||||
throw salesforceError;
|
throw salesforceError;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error closing previous session:", error);
|
console.error("Error closing previous session:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -243,202 +120,36 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
||||||
async function va_project_log_callback(req, res) {
|
async function va_project_log_callback(req, res) {
|
||||||
try {
|
try {
|
||||||
const payload = req.body || {};
|
const payload = req.body || {};
|
||||||
const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload;
|
const { vaId, vaEmail, projectId, type } = payload;
|
||||||
// console.log("Received payload:", payload);
|
|
||||||
|
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
|
||||||
if (!vaId || !vaEmail || !projectId || !type) {
|
|
||||||
return res.status(400).send("Missing required properties");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the log time based on received time (both in UTC)
|
|
||||||
const currentTime = getCurrentUTCTime(); // Current UTC time
|
|
||||||
let logTime = currentTime;
|
|
||||||
|
|
||||||
if (receivedTimeString) {
|
|
||||||
// Parse the received ISO time string
|
|
||||||
const receivedTime = parseISOTimeString(receivedTimeString);
|
|
||||||
|
|
||||||
// Check if the difference is less than 10 seconds
|
|
||||||
if (isTimeWithinThreshold(receivedTime, currentTime, 10)) {
|
|
||||||
logTime = receivedTime;
|
|
||||||
console.log(`✅ Using received ISO time: ${receivedTimeString}, difference is within 10 seconds`);
|
|
||||||
console.log(`✅ Parsed as: ${receivedTime.toISOString()}`);
|
|
||||||
} else {
|
|
||||||
logTime = currentTime;
|
|
||||||
console.log(`⚠️ Using current UTC time, received time difference exceeds 10 seconds.`);
|
|
||||||
console.log(`Received: ${receivedTimeString} (parsed: ${receivedTime.toISOString()})`);
|
|
||||||
console.log(`Current: ${currentTime.toISOString()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const project = await ProjectModel.findById(projectId);
|
const project = await ProjectModel.findById(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return res.status(400).json({ success: false, message: "Project not found" });
|
return res.status(400).json({ success: false, message: "Project not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaUser = await VAModel.findById(vaId);
|
// For LOGIN requests, check daily hours
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(400).json({ success: false, message: "VA not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check schedule compliance
|
|
||||||
// const scheduleCheck = checkScheduleCompliance(logTime, project, vaUser.status, type);
|
|
||||||
// console.log(`Schedule compliance check:`, scheduleCheck);
|
|
||||||
// if (!scheduleCheck.isCompliant) {
|
|
||||||
// return res.status(400).json({
|
|
||||||
// success: false,
|
|
||||||
// message: `Cannot ${type.toLowerCase()} - outside of allowed schedule time`,
|
|
||||||
// warnings: scheduleCheck.warnings
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// If Logout then Update work track entries
|
|
||||||
if (type === "LOGOUT") {
|
|
||||||
const { logId } = payload;
|
|
||||||
|
|
||||||
const workTrackData = await WorkTrackModel.find({
|
|
||||||
logId: new mongoose.Types.ObjectId(logId)
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log(`Found work track data for logout:`, workTrackData);
|
|
||||||
|
|
||||||
if (workTrackData[0]) {
|
|
||||||
const fromLogTime = new Date(workTrackData[0].from);
|
|
||||||
const toLogTime = new Date(logTime);
|
|
||||||
const totalTimeMs = toLogTime - fromLogTime;
|
|
||||||
|
|
||||||
const updatedLogs = workTrackData[0].logs ? [...workTrackData[0].logs] : [];
|
|
||||||
|
|
||||||
if (updatedLogs.length > 0) {
|
|
||||||
const lastLogIndex = updatedLogs.length - 1;
|
|
||||||
const lastLogEntry = updatedLogs[lastLogIndex];
|
|
||||||
|
|
||||||
// Update the last log's 'to' time and recalculate its totalTime
|
|
||||||
const lastLogFromTime = new Date(lastLogEntry.from);
|
|
||||||
const lastLogTotalTime = Math.abs(toLogTime - lastLogFromTime);
|
|
||||||
|
|
||||||
updatedLogs[lastLogIndex] = {
|
|
||||||
...lastLogEntry,
|
|
||||||
to: toLogTime,
|
|
||||||
totalTime: lastLogTotalTime
|
|
||||||
};
|
|
||||||
|
|
||||||
// console.log(`Updated last log in array - from: ${lastLogFromTime.toISOString()}, to: ${toLogTime.toISOString()}, totalTime: ${lastLogTotalTime}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateResult = await WorkTrackModel.updateOne(
|
|
||||||
{ _id: workTrackData[0]._id },
|
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
to: toLogTime,
|
|
||||||
totalTime: totalTimeMs,
|
|
||||||
logs: updatedLogs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Updated work track ${workTrackData[0]._id}:`, {
|
|
||||||
from: fromLogTime.toISOString(),
|
|
||||||
to: toLogTime.toISOString(),
|
|
||||||
totalTime: totalTimeMs,
|
|
||||||
logsUpdated: updatedLogs.length,
|
|
||||||
mongodbUpdateResult: updateResult
|
|
||||||
});
|
|
||||||
|
|
||||||
// const updatedWorkTrack = await WorkTrackModel.findById(workTrackData[0]._id);
|
|
||||||
// console.log("Verified updated work track:", {
|
|
||||||
// to: updatedWorkTrack?.to,
|
|
||||||
// totalTime: updatedWorkTrack?.totalTime,
|
|
||||||
// logsCount: updatedWorkTrack?.logs?.length,
|
|
||||||
// lastLog: updatedWorkTrack?.logs?.[updatedWorkTrack.logs.length - 1]
|
|
||||||
// });
|
|
||||||
} else {
|
|
||||||
console.log(`No work track found for vaId: ${vaId}, projectId: ${projectId}, logId: ${logId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the logout log entry
|
|
||||||
const createdLog = await LoggedModel.create({
|
|
||||||
loggedType: type,
|
|
||||||
vaId,
|
|
||||||
vaEmail,
|
|
||||||
projectId,
|
|
||||||
time: logTime
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
|
||||||
const sfData = {
|
|
||||||
"_id": createdLog._id,
|
|
||||||
"loggedType": type,
|
|
||||||
"vaId": vaId,
|
|
||||||
"vaEmail": vaEmail,
|
|
||||||
"projectId": projectId,
|
|
||||||
"time": createdLog.time,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(sfData)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
return res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
message: `${type} Successfully`,
|
|
||||||
logId: createdLog._id,
|
|
||||||
SyncSalesforce: result,
|
|
||||||
timeUsed: receivedTimeString ? 'received' : 'current',
|
|
||||||
actualTime: createdLog.time,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// Still return success since the logout was recorded locally
|
|
||||||
return res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
message: `${type} Successfully (Salesforce sync failed)`,
|
|
||||||
logId: createdLog._id,
|
|
||||||
timeUsed: receivedTimeString ? 'received' : 'current',
|
|
||||||
actualTime: createdLog.time,
|
|
||||||
warning: "Salesforce sync failed"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For LOGIN requests, check daily hours (using UTC day range)
|
|
||||||
if (type === "LOGIN") {
|
if (type === "LOGIN") {
|
||||||
const { start, end } = getDayRange(); // UTC day range
|
const { start, end } = getDayRange();
|
||||||
// Get today's total hours
|
// Get today's total hours
|
||||||
const todayWorkEntries = await WorkTrackModel.find({
|
const todayWorkEntries = await WorkTrackModel.find({
|
||||||
vaId,
|
vaId,
|
||||||
projectId,
|
projectId,
|
||||||
from: { $gte: start, $lte: end }
|
from: { $gte: start, $lte: end }
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
const totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
||||||
return total + (entry.totalTime || 0);
|
return total + (entry.totalTime || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
||||||
|
|
||||||
const dayNumber = getDayNumber(logTime);
|
|
||||||
|
|
||||||
// Check if daily hours are already completed
|
// Check if daily hours are already completed
|
||||||
if (project.dedicatedHours[dayNumber] > 0 && totalTodayHours >= project.dedicatedHours[dayNumber]) {
|
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours[dayNumber])})`,
|
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`
|
||||||
// warnings: scheduleCheck.warnings
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -452,131 +163,75 @@ async function va_project_log_callback(req, res) {
|
||||||
|
|
||||||
// check last workModel login time and make a logout request then create login
|
// check last workModel login time and make a logout request then create login
|
||||||
// console.log(`User already logged in. Closing previous session...`);
|
// console.log(`User already logged in. Closing previous session...`);
|
||||||
const closeResult = await closePreviousSession(vaId, lastLog, logTime);
|
const closeResult = await closePreviousSession(vaId, lastLog);
|
||||||
|
|
||||||
// console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`);
|
// console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`);
|
||||||
|
|
||||||
// Now proceed to create new login
|
// Now proceed to create new login
|
||||||
const createdLog = await LoggedModel.create({
|
const createdLog = await LoggedModel.create({
|
||||||
loggedType: "LOGIN",
|
loggedType: "LOGIN",
|
||||||
vaId,
|
vaId,
|
||||||
vaEmail,
|
vaEmail,
|
||||||
projectId,
|
projectId
|
||||||
time: logTime
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
return res.status(201).send({
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
success: true,
|
||||||
const sfData = {
|
message: `Login Successful (previous session closed)`,
|
||||||
"_id": createdLog._id,
|
logId: createdLog._id,
|
||||||
"loggedType": "LOGIN",
|
previousSession: {
|
||||||
"vaId": vaId,
|
closedWorkTracks: closeResult.closedWorkTracks,
|
||||||
"vaEmail": vaEmail,
|
logoutLogId: closeResult.logoutLogId
|
||||||
"projectId": projectId,
|
|
||||||
"time": createdLog.time,
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
const response = await authFetch(salesforceUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify(sfData)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
||||||
|
|
||||||
return res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
message: `${type} Successfully`,
|
|
||||||
logId: createdLog._id,
|
|
||||||
SyncSalesforce: result,
|
|
||||||
previousSession: {
|
|
||||||
closedWorkTracks: closeResult.closedWorkTracks,
|
|
||||||
logoutLogId: closeResult.logoutLogId
|
|
||||||
},
|
|
||||||
timeUsed: receivedTimeString ? 'received' : 'current',
|
|
||||||
actualTime: createdLog.time,
|
|
||||||
// warnings: scheduleCheck.warnings
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
salesforceError.isSalesforceError = true;
|
|
||||||
throw salesforceError;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdLog = await LoggedModel.create({
|
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
|
||||||
loggedType: type,
|
// console.log(createdLog)
|
||||||
vaId,
|
// try {
|
||||||
vaEmail,
|
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||||
projectId,
|
// const sfData = {
|
||||||
time: logTime
|
// "_id": createdLog._id,
|
||||||
});
|
// "loggedType": type,
|
||||||
|
// "vaId": vaId,
|
||||||
|
// "vaEmail": vaEmail,
|
||||||
|
// "projectId": projectId,
|
||||||
|
// "time": createdLog.time,
|
||||||
|
// }
|
||||||
|
|
||||||
try {
|
// const response = await fetch(salesforceUrl, {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
// method: 'POST',
|
||||||
const sfData = {
|
// headers: {
|
||||||
"_id": createdLog._id,
|
// 'Content-Type': 'application/json',
|
||||||
"loggedType": type,
|
// // Add any required headers for Salesforce authentication
|
||||||
"vaId": vaId,
|
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
"vaEmail": vaEmail,
|
// },
|
||||||
"projectId": projectId,
|
// body: JSON.stringify(sfData)
|
||||||
"time": createdLog.time,
|
// });
|
||||||
}
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// const errorText = await response.text();
|
||||||
|
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
|
// }
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
// const result = await response.json();
|
||||||
method: 'POST',
|
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
// res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
|
||||||
// Add any required headers for Salesforce authentication
|
// } catch (error) {
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
// console.error("Failed to sync with Salesforce:", error.message);
|
||||||
},
|
// console.log(error)
|
||||||
body: JSON.stringify(sfData)
|
|
||||||
});
|
// // Create a custom error to distinguish Salesforce errors
|
||||||
|
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
|
// salesforceError.isSalesforceError = true;
|
||||||
|
// throw salesforceError;
|
||||||
|
// }
|
||||||
|
|
||||||
if (!response.ok) {
|
return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
message: `${type} Successfully`,
|
|
||||||
logId: createdLog._id,
|
|
||||||
SyncSalesforce: result,
|
|
||||||
timeUsed: receivedTimeString ? 'received' : 'current',
|
|
||||||
actualTime: createdLog.time,
|
|
||||||
// warnings: scheduleCheck.warnings
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
salesforceError.isSalesforceError = true;
|
|
||||||
throw salesforceError;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in va_project_log_callback:", error);
|
|
||||||
return res.status(500).send("Internal Server Error");
|
return res.status(500).send("Internal Server Error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
|
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
|
||||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
|
||||||
|
|
||||||
function formatHMS(ms) {
|
function formatHMS(ms) {
|
||||||
const totalSeconds = Math.floor(ms / 1000);
|
const totalSeconds = Math.floor(ms / 1000);
|
||||||
|
|
@ -73,7 +72,7 @@ async function va_projects_log_list_function(req, res) {
|
||||||
|
|
||||||
console.log("VA ID:", vaId.toString());
|
console.log("VA ID:", vaId.toString());
|
||||||
|
|
||||||
projectList = await ProjectModel.find({ "vaAssigned.vaId": vaUser._id });
|
projectList = await ProjectModel.find({ vaAssigned: vaUser._id });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!projectList.length) {
|
if (!projectList.length) {
|
||||||
|
|
@ -111,15 +110,13 @@ async function va_projects_log_list_function(req, res) {
|
||||||
const lastLog = logs[logs.length - 1];
|
const lastLog = logs[logs.length - 1];
|
||||||
const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY";
|
const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY";
|
||||||
|
|
||||||
const dayNumber = getDayNumber();
|
|
||||||
|
|
||||||
finalReport.push({
|
finalReport.push({
|
||||||
projectId: project._id,
|
projectId: project._id,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
activityStatus,
|
activityStatus,
|
||||||
totalWorkTime: formatHMS(totalWorkMs),
|
totalWorkTime: formatHMS(totalWorkMs),
|
||||||
totalWorkMs,
|
totalWorkMs,
|
||||||
dedicatedHours: formatHoursToReadable(project?.dedicatedHours[dayNumber]) || "0h",
|
dedicatedHours: formatHoursToReadable(project?.dedicatedHours),
|
||||||
totalLogs: logs.length,
|
totalLogs: logs.length,
|
||||||
status: project?.status
|
status: project?.status
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,36 +11,23 @@ function formatHMS(ms) {
|
||||||
|
|
||||||
async function va_projects_report_function(req, res) {
|
async function va_projects_report_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
const accessToken = req.headers.token;
|
||||||
let vaId;
|
if (!accessToken) {
|
||||||
let vaUser;
|
return res.status(401).json({ success: false, message: "Missing token" });
|
||||||
let vaEmail;
|
|
||||||
if(sf_token) {
|
|
||||||
vaId = req.body.vaId;
|
|
||||||
vaUser = await VAModel.findById(vaId);
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
|
||||||
}
|
|
||||||
vaEmail = vaUser.userDetails.email
|
|
||||||
} else {
|
|
||||||
const accessToken = req.headers.token;
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ success: false, message: "Missing token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
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" });
|
|
||||||
}
|
|
||||||
|
|
||||||
vaEmail = decoded.email;
|
|
||||||
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
vaId = vaUser._id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
const { projectId, from, to } = req.params;
|
const { projectId, from, to } = req.params;
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
|
|
@ -49,38 +36,9 @@ async function va_projects_report_function(req, res) {
|
||||||
|
|
||||||
// Default: current day range
|
// Default: current day range
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let fromDate, toDate;
|
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();
|
||||||
if (from && from !== "undefined" && from !== "null") {
|
toDate.setHours(23, 59, 59, 999);
|
||||||
fromDate = new Date(from);
|
|
||||||
// If only date string provided (YYYY-MM-DD), set to start of day
|
|
||||||
if (from.length === 10) {
|
|
||||||
fromDate.setHours(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default: 15 days ago
|
|
||||||
fromDate = new Date();
|
|
||||||
fromDate.setHours(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to && to !== "undefined" && to !== "null") {
|
|
||||||
toDate = new Date(to);
|
|
||||||
// If only date string provided (YYYY-MM-DD), set to end of day
|
|
||||||
if (to.length === 10) {
|
|
||||||
toDate.setHours(23, 59, 59, 999);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default: today end of day
|
|
||||||
toDate = new Date();
|
|
||||||
toDate.setHours(23, 59, 59, 999);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromDate > toDate) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Invalid date range: 'from' date must be before 'to' date"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all VA login/logout logs for this project in range
|
// Get all VA login/logout logs for this project in range
|
||||||
const logs = await LoggedModel.find({
|
const logs = await LoggedModel.find({
|
||||||
|
|
@ -90,35 +48,8 @@ async function va_projects_report_function(req, res) {
|
||||||
}).sort({ time: 1 }).lean();
|
}).sort({ time: 1 }).lean();
|
||||||
|
|
||||||
if (!logs.length) {
|
if (!logs.length) {
|
||||||
return res.json({
|
return res.json({ success: true, message: "No logs found for this project", data: [] });
|
||||||
success: true,
|
|
||||||
message: "No logs found for this project in the specified date range",
|
|
||||||
data: [],
|
|
||||||
dateRange: {
|
|
||||||
from: fromDate.toISOString().split('T')[0],
|
|
||||||
to: toDate.toISOString().split('T')[0],
|
|
||||||
days: Math.ceil((toDate - fromDate) / (1000 * 60 * 60 * 24))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all work tracks for this VA and project in the date range
|
|
||||||
const workTracks = await WorkTrackModel.find({
|
|
||||||
projectId,
|
|
||||||
vaId,
|
|
||||||
$or: [
|
|
||||||
{ from: { $gte: fromDate, $lte: toDate } },
|
|
||||||
{ to: { $gte: fromDate, $lte: toDate } },
|
|
||||||
{
|
|
||||||
$and: [
|
|
||||||
{ from: { $lte: fromDate } },
|
|
||||||
{ to: { $gte: fromDate } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.sort({ from: 1 })
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
// Unique VA IDs
|
// Unique VA IDs
|
||||||
const vaIds = [...new Set(logs.map(log => log.vaId?.toString()))].filter(Boolean);
|
const vaIds = [...new Set(logs.map(log => log.vaId?.toString()))].filter(Boolean);
|
||||||
|
|
@ -129,120 +60,55 @@ async function va_projects_report_function(req, res) {
|
||||||
|
|
||||||
const projectReport = [];
|
const projectReport = [];
|
||||||
|
|
||||||
for (const currentVaId of vaIds) {
|
for (const vaId of vaIds) {
|
||||||
const vaLogList = logs.filter(l => l.vaId.toString() === currentVaId);
|
const vaLogList = logs.filter(l => l.vaId.toString() === vaId);
|
||||||
let sessions = [];
|
let sessions = [];
|
||||||
let currentLogin = null;
|
let currentLogin = null;
|
||||||
const vaWorkTracks = workTracks.filter(w => w.vaId.toString() === currentVaId);
|
|
||||||
|
|
||||||
// Pair login → logout
|
// Pair login → logout
|
||||||
for (const entry of vaLogList) {
|
for (const entry of vaLogList) {
|
||||||
if (entry.loggedType === "LOGIN") {
|
if (entry.loggedType === "LOGIN") {
|
||||||
currentLogin = entry;
|
currentLogin = entry;
|
||||||
} else if (entry.loggedType === "LOGOUT" && currentLogin) {
|
} else if (entry.loggedType === "LOGOUT" && currentLogin) {
|
||||||
sessions.push({ from: currentLogin.time, to: entry.time, logId: currentLogin._id });
|
sessions.push({ from: currentLogin.time, to: entry.time });
|
||||||
currentLogin = null;
|
currentLogin = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If still logged in, close at end of day
|
// If still logged in, close at end of day
|
||||||
if (currentLogin) {
|
if (currentLogin) {
|
||||||
sessions.push({ from: currentLogin.time, to: toDate, logId: currentLogin._id });
|
sessions.push({ from: currentLogin.time, to: toDate });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaDetails = vaUsers.find(v => v._id.toString() === currentVaId);
|
const vaDetails = vaUsers.find(v => v._id.toString() === vaId);
|
||||||
let totalWorkMs = 0;
|
let totalWorkMs = 0;
|
||||||
const sessionDetails = [];
|
const sessionDetails = [];
|
||||||
|
|
||||||
// for (const s of sessions) {
|
for (const s of sessions) {
|
||||||
// const workData = await WorkTrackModel.find({
|
const workData = await WorkTrackModel.find({
|
||||||
// vaId,
|
vaId,
|
||||||
// projectId,
|
projectId,
|
||||||
// from: { $gte: s.from },
|
from: { $gte: s.from },
|
||||||
// to: { $lte: s.to }
|
to: { $lte: s.to }
|
||||||
// }).sort({ from: 1 }).lean();
|
}).sort({ from: 1 }).lean();
|
||||||
|
|
||||||
// const tabs = workData.map(t => ({
|
const tabs = workData.map(t => ({
|
||||||
// title: t.title,
|
title: t.title,
|
||||||
// appName: t.appName,
|
appName: t.appName,
|
||||||
// from: t.from,
|
from: t.from,
|
||||||
// to: t.to,
|
to: t.to,
|
||||||
// durationMs: t.totalTime,
|
durationMs: t.totalTime,
|
||||||
// durationFormatted: formatHMS(t.totalTime),
|
durationFormatted: formatHMS(t.totalTime),
|
||||||
// }));
|
}));
|
||||||
|
|
||||||
// const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0);
|
const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0);
|
||||||
// totalWorkMs += sessionWorkMs;
|
totalWorkMs += sessionWorkMs;
|
||||||
|
|
||||||
// sessionDetails.push({
|
|
||||||
// from: s.from,
|
|
||||||
// to: s.to,
|
|
||||||
// totalSessionTime: formatHMS(sessionWorkMs),
|
|
||||||
// tabs,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
for (const session of sessions) {
|
|
||||||
// Find work tracks for this specific session (matching logId)
|
|
||||||
const sessionWorkTracks = vaWorkTracks.filter(
|
|
||||||
track => track.logId && track.logId.toString() === session.logId.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
let sessionTabs = [];
|
|
||||||
let sessionTotalMs = 0;
|
|
||||||
|
|
||||||
// Extract all logs from work tracks for this session
|
|
||||||
for (const workTrack of sessionWorkTracks) {
|
|
||||||
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
|
||||||
// Process each log entry in the logs array
|
|
||||||
for (const logEntry of workTrack.logs) {
|
|
||||||
// Calculate duration for this log entry
|
|
||||||
const logFrom = new Date(logEntry.from);
|
|
||||||
const logTo = new Date(logEntry.to || workTrack.to || session.to);
|
|
||||||
const logDuration = logEntry.totalTime || (logTo - logFrom);
|
|
||||||
|
|
||||||
sessionTotalMs += logDuration;
|
|
||||||
|
|
||||||
sessionTabs.push({
|
|
||||||
title: logEntry.title || "",
|
|
||||||
appName: logEntry.appName || "",
|
|
||||||
url: logEntry.url || null,
|
|
||||||
from: logFrom,
|
|
||||||
to: logTo,
|
|
||||||
durationMs: logDuration,
|
|
||||||
durationFormatted: formatHMS(logDuration),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also add the main work track if it has separate data
|
|
||||||
if (workTrack.totalTime && workTrack.totalTime > 0) {
|
|
||||||
sessionTotalMs += workTrack.totalTime;
|
|
||||||
|
|
||||||
// Only add if we don't have logs array data
|
|
||||||
if (!workTrack.logs || workTrack.logs.length === 0) {
|
|
||||||
sessionTabs.push({
|
|
||||||
title: workTrack.title || "",
|
|
||||||
appName: workTrack.appName || "",
|
|
||||||
url: workTrack.url || null,
|
|
||||||
from: workTrack.from,
|
|
||||||
to: workTrack.to,
|
|
||||||
durationMs: workTrack.totalTime,
|
|
||||||
durationFormatted: formatHMS(workTrack.totalTime),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWorkMs += sessionTotalMs;
|
|
||||||
|
|
||||||
sessionDetails.push({
|
sessionDetails.push({
|
||||||
from: session.from,
|
from: s.from,
|
||||||
to: session.to,
|
to: s.to,
|
||||||
totalSessionTime: formatHMS(sessionTotalMs),
|
totalSessionTime: formatHMS(sessionWorkMs),
|
||||||
totalSessionMs: sessionTotalMs,
|
tabs,
|
||||||
workTracksCount: sessionWorkTracks.length,
|
|
||||||
tabs: sessionTabs.sort((a, b) => a.from - b.from), // Sort tabs by time
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,15 @@
|
||||||
import mongoose from "mongoose";
|
|
||||||
import { AdjustedHoursModel } from "../../../models/vaModel.js";
|
import { AdjustedHoursModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
// Get all adjustment requests (with filtering options)
|
// Get all adjustment requests (with filtering options)
|
||||||
async function get_adjustment_requests_list(req, res) {
|
async function get_adjustment_requests_list(req, res) {
|
||||||
try {
|
try {
|
||||||
const { status, projectId, vaId } = req.query;
|
const { status, projectId, vaId } = req.params;
|
||||||
|
|
||||||
// Build filter object
|
// Build filter object
|
||||||
const filter = {};
|
const filter = {};
|
||||||
// if (status) filter.status = status;
|
if (status) filter.status = status;
|
||||||
// if (projectId) filter.projectId = projectId;
|
if (projectId) filter.projectId = projectId;
|
||||||
// if (vaId) filter.vaId = vaId;
|
if (vaId) filter.vaId = vaId;
|
||||||
|
|
||||||
if (status && status !== "undefined") filter.status = status;
|
|
||||||
if (projectId && mongoose.Types.ObjectId.isValid(projectId)) {
|
|
||||||
filter.projectId = new mongoose.Types.ObjectId(projectId);
|
|
||||||
}
|
|
||||||
if (vaId && mongoose.Types.ObjectId.isValid(vaId)) {
|
|
||||||
filter.vaId = new mongoose.Types.ObjectId(vaId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const adjustments = await AdjustedHoursModel.find(filter)
|
const adjustments = await AdjustedHoursModel.find(filter)
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
|
|
@ -63,9 +54,9 @@ async function get_adjustment_requests_list(req, res) {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Get adjustment requests error:", error);
|
console.error("Get adjustment requests error:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Internal Server Error"
|
message: "Internal Server Error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ async function va_timesheet_manage_adjustment_request(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "APPROVE") {
|
if (action === "approve") {
|
||||||
// Create new log entries in LoggedModel
|
// Create new log entries in LoggedModel
|
||||||
const loginLog = new LoggedModel({
|
const loginLog = new LoggedModel({
|
||||||
loggedType: "LOGIN",
|
loggedType: "LOGIN",
|
||||||
|
|
@ -78,7 +78,7 @@ async function va_timesheet_manage_adjustment_request(req, res) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (action === "REJECT") {
|
} else if (action === "reject") {
|
||||||
// Update adjustment record as rejected
|
// Update adjustment record as rejected
|
||||||
adjustment.status = "REJECTED";
|
adjustment.status = "REJECTED";
|
||||||
adjustment.reviewedAt = new Date();
|
adjustment.reviewedAt = new Date();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
|
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
// Adjust missing hours by creating new records
|
// Adjust missing hours by creating new records
|
||||||
async function va_timesheet_adjustment_function(req, res) {
|
async function va_timesheet_adjustment_function(req, res) {
|
||||||
|
|
@ -35,7 +34,7 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
// Validate project exists and VA is assigned to it
|
// Validate project exists and VA is assigned to it
|
||||||
const project = await ProjectModel.findOne({
|
const project = await ProjectModel.findOne({
|
||||||
_id: projectId,
|
_id: projectId,
|
||||||
"vaAssigned.vaId": vaId
|
vaAssigned: vaId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
|
@ -60,6 +59,14 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
|
|
||||||
const combineDateTime = (dateObj, timeStr) => {
|
const combineDateTime = (dateObj, timeStr) => {
|
||||||
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
||||||
|
|
||||||
|
// // Extract date components from the Date object
|
||||||
|
// const year = dateObj.getFullYear();
|
||||||
|
// const month = dateObj.getMonth();
|
||||||
|
// const day = dateObj.getDate();
|
||||||
|
|
||||||
|
// // Create UTC date directly
|
||||||
|
// return new Date(year, month, day, hours, minutes, seconds);
|
||||||
const combined = new Date(dateObj);
|
const combined = new Date(dateObj);
|
||||||
combined.setHours(hours, minutes, seconds, 0);
|
combined.setHours(hours, minutes, seconds, 0);
|
||||||
|
|
||||||
|
|
@ -113,16 +120,13 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedComment = processHTMLComment(comment);
|
|
||||||
|
|
||||||
const adjustmentRequest = new AdjustedHoursModel({
|
const adjustmentRequest = new AdjustedHoursModel({
|
||||||
vaId,
|
vaId,
|
||||||
projectId,
|
projectId,
|
||||||
date: startOfDay,
|
date: startOfDay,
|
||||||
loginTime: loginDateTime,
|
loginTime: loginDateTime,
|
||||||
logoutTime: logoutDateTime,
|
logoutTime: logoutDateTime,
|
||||||
comment: processedComment.processedHTML,
|
comment,
|
||||||
attachments: processedComment.extractedImages,
|
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -139,24 +143,23 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
||||||
const sfData = {
|
const sfData = {
|
||||||
"id": adjustmentRequest._id.toString(),
|
"id": adjustmentRequest._id,
|
||||||
"va": {
|
"va": {
|
||||||
"id": vaId.toString()
|
"id": vaId
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"id": projectId.toString()
|
"id": projectId
|
||||||
},
|
},
|
||||||
"date": adjustmentDate.toISOString().split('T')[0],
|
"date": adjustmentDate.toISOString().split('T')[0],
|
||||||
"loginTime": loginDateTime.toISOString(),
|
"loginTime": loginDateTime.toISOString(),
|
||||||
"logoutTime": logoutDateTime.toISOString(),
|
"logoutTime": logoutDateTime.toISOString(),
|
||||||
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
|
"comment": comment,
|
||||||
"attachments": processedComment.extractedImages, // Send extracted images info
|
|
||||||
"status": "PENDING",
|
"status": "PENDING",
|
||||||
"rejectionReason": "",
|
"rejectionReason": "",
|
||||||
"adjustedHours": formatTime(adjustedSeconds)
|
"adjustedHours": formatTime(adjustedSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -168,7 +171,6 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
await AdjustedHoursModel.findByIdAndDelete(adjustmentRequest._id)
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,8 +186,7 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
loginTime: loginDateTime.toISOString(),
|
loginTime: loginDateTime.toISOString(),
|
||||||
logoutTime: logoutDateTime.toISOString(),
|
logoutTime: logoutDateTime.toISOString(),
|
||||||
adjustedHours: formatTime(adjustedSeconds),
|
adjustedHours: formatTime(adjustedSeconds),
|
||||||
comment: processedComment.processedHTML,
|
comment: comment,
|
||||||
attachments: processedComment.extractedImages,
|
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
},
|
},
|
||||||
SyncSalesforce: result
|
SyncSalesforce: result
|
||||||
|
|
@ -220,116 +221,4 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processHTMLComment(htmlComment) {
|
|
||||||
try {
|
|
||||||
let processedHTML = htmlComment;
|
|
||||||
const extractedImages = [];
|
|
||||||
let plainText = '';
|
|
||||||
|
|
||||||
// Extract plain text from HTML for Salesforce
|
|
||||||
plainText = extractPlainTextFromHTML(htmlComment);
|
|
||||||
|
|
||||||
// Process base64 images in HTML
|
|
||||||
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
|
|
||||||
processedHTML = imageProcessingResult.processedHTML;
|
|
||||||
extractedImages.push(...imageProcessingResult.extractedImages);
|
|
||||||
|
|
||||||
return {
|
|
||||||
processedHTML,
|
|
||||||
plainText,
|
|
||||||
extractedImages
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing HTML comment:", error);
|
|
||||||
// Return original content if processing fails
|
|
||||||
return {
|
|
||||||
processedHTML: htmlComment,
|
|
||||||
plainText: extractPlainTextFromHTML(htmlComment),
|
|
||||||
extractedImages: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract plain text from HTML
|
|
||||||
function extractPlainTextFromHTML(html) {
|
|
||||||
try {
|
|
||||||
// Remove HTML tags and decode HTML entities
|
|
||||||
let plainText = html
|
|
||||||
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
|
|
||||||
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// Decode common HTML entities
|
|
||||||
plainText = plainText
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(///g, '/');
|
|
||||||
|
|
||||||
return plainText;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error extracting plain text from HTML:", error);
|
|
||||||
return html.replace(/<[^>]*>/g, ' ').trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract and process base64 images from HTML
|
|
||||||
function extractAndProcessBase64Images(html) {
|
|
||||||
const extractedImages = [];
|
|
||||||
let processedHTML = html;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Regex to find base64 images in HTML (various formats)
|
|
||||||
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
let imageIndex = 0;
|
|
||||||
|
|
||||||
while ((match = base64Regex.exec(html)) !== null) {
|
|
||||||
const fullMatch = match[0];
|
|
||||||
const imageType = match[1]; // png, jpeg, gif, etc.
|
|
||||||
const base64Data = match[2];
|
|
||||||
|
|
||||||
// Create a unique filename
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const filename = `adjustment_image_${timestamp}_${imageIndex}.${imageType}`;
|
|
||||||
|
|
||||||
// Calculate approximate file size
|
|
||||||
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
|
|
||||||
|
|
||||||
// Store image information
|
|
||||||
extractedImages.push({
|
|
||||||
filename: filename,
|
|
||||||
mimeType: `image/${imageType}`,
|
|
||||||
base64Data: base64Data,
|
|
||||||
size: size,
|
|
||||||
originalHTML: fullMatch, // Store original img tag for reference
|
|
||||||
index: imageIndex
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace base64 with a reference in HTML
|
|
||||||
const replacementHTML = `<img src="attachment://${filename}" alt="Adjustment evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="adjustment-image">`;
|
|
||||||
|
|
||||||
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
|
|
||||||
|
|
||||||
imageIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
processedHTML,
|
|
||||||
extractedImages
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing base64 images:", error);
|
|
||||||
return {
|
|
||||||
processedHTML: html,
|
|
||||||
extractedImages: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { va_timesheet_adjustment_function };
|
export { va_timesheet_adjustment_function };
|
||||||
|
|
@ -1,40 +1,26 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
|
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
|
||||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
|
||||||
|
|
||||||
async function va_timesheet_summary_function(req, res) {
|
async function va_timesheet_summary_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
const accessToken = req.headers.token;
|
||||||
let vaId;
|
|
||||||
let vaUser;
|
if (!accessToken) {
|
||||||
let vaEmail;
|
return res.status(401).json({ success: false, message: "Missing token" });
|
||||||
if(sf_token) {
|
|
||||||
vaId = req.body.vaId;
|
|
||||||
vaUser = await VAModel.findById(vaId);
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
|
||||||
}
|
|
||||||
vaEmail = vaUser.userDetails.email
|
|
||||||
} else {
|
|
||||||
const accessToken = req.headers.token;
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ success: false, message: "Missing token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
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" });
|
|
||||||
}
|
|
||||||
|
|
||||||
vaEmail = decoded.email;
|
|
||||||
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
vaId = vaUser._id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
const { projectId, date } = req.params;
|
const { projectId, date } = req.params;
|
||||||
|
|
||||||
if (!projectId || !date) {
|
if (!projectId || !date) {
|
||||||
|
|
@ -44,7 +30,7 @@ async function va_timesheet_summary_function(req, res) {
|
||||||
// Validate project exists and VA is assigned to it
|
// Validate project exists and VA is assigned to it
|
||||||
const project = await ProjectModel.findOne({
|
const project = await ProjectModel.findOne({
|
||||||
_id: projectId,
|
_id: projectId,
|
||||||
"vaAssigned.vaId": vaId
|
vaAssigned: vaId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
|
@ -163,10 +149,8 @@ async function va_timesheet_summary_function(req, res) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dayNumber = getDayNumber();
|
|
||||||
|
|
||||||
// Calculate dedicated hours and pending hours
|
// Calculate dedicated hours and pending hours
|
||||||
const dedicatedHours = project.dedicatedHours[dayNumber] || 0;
|
const dedicatedHours = project.dedicatedHours || 0;
|
||||||
const totalWorkedHours = totalWorkSeconds / 3600; // Convert to hours
|
const totalWorkedHours = totalWorkSeconds / 3600; // Convert to hours
|
||||||
const pendingHours = Math.max(0, dedicatedHours - totalWorkedHours);
|
const pendingHours = Math.max(0, dedicatedHours - totalWorkedHours);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js";
|
|
||||||
import authFetch from "../../../utils/authFetch.js";
|
|
||||||
|
|
||||||
// Approve or reject leave
|
|
||||||
async function va_tips_manage_function(req, res) {
|
|
||||||
|
|
||||||
const { clientId, amount, description, vaId } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const vaUser = await VAModel.findById(vaId);
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Virtual Assistant not found" });
|
|
||||||
}
|
|
||||||
const clUser = await ClientModel.findById(clientId);
|
|
||||||
if (!clUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Client not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new tip entry
|
|
||||||
const newTip = {
|
|
||||||
clientId,
|
|
||||||
status: "Unbilled",
|
|
||||||
amount: parseFloat(amount).toFixed(2),
|
|
||||||
description: description || "",
|
|
||||||
vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "",
|
|
||||||
vaId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createdTip = await TipsModel.create(newTip);
|
|
||||||
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
|
|
||||||
|
|
||||||
const sfData = {
|
|
||||||
"id": createdTip._id.toString(),
|
|
||||||
"vaId": createdTip.vaId.toString(),
|
|
||||||
"status": "Unbilled",
|
|
||||||
"clientId": createdTip.clientId.toString(),
|
|
||||||
"comment": createdTip.description || "",
|
|
||||||
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ ...sfData })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log("✅ Successfully synced Tip data to Salesforce");
|
|
||||||
return res.status(201).json({ success: true, data: createdTip });
|
|
||||||
} else {
|
|
||||||
TipsModel.findByIdAndDelete(createdTip._id);
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function va_tips_get(req, res) {
|
|
||||||
|
|
||||||
const { clientId } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clUser = await ClientModel.findById(clientId);
|
|
||||||
if (!clUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Client not found" });
|
|
||||||
}
|
|
||||||
const data = await TipsModel.find({ clientId: clientId, status: "Unbilled" });
|
|
||||||
return res.status(200).json({ success: true, data: data });
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function va_tips_update(req, res) {
|
|
||||||
const { tipId } = req.params;
|
|
||||||
const { update } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tipData = await TipsModel.findById(tipId);
|
|
||||||
if (!tipData) {
|
|
||||||
return res.status(404).json({ success: false, message: "Tip not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await TipsModel.updateOne({ _id: tipId }, update);
|
|
||||||
return res.status(200).json({ success: true, message: "Tip updated successfully" });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function va_tips_delete(req, res) {
|
|
||||||
const { tipId } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
|
|
||||||
|
|
||||||
// const sfData = {
|
|
||||||
// "id": createdTip._id.toString(),
|
|
||||||
// "vaId": createdTip.vaId.toString(),
|
|
||||||
// "status": "Unbilled",
|
|
||||||
// "clientId": createdTip.clientId.toString(),
|
|
||||||
// "comment": createdTip.description,
|
|
||||||
// "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
|
|
||||||
// }
|
|
||||||
|
|
||||||
const response = await authFetch(salesforceUrl, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ "id": tipId})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await TipsModel.findOneAndDelete({ _id: tipId });
|
|
||||||
return res.status(200).json({ success: true, message: "Tip updated successfully" });
|
|
||||||
} else {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export { va_tips_manage_function, va_tips_get, va_tips_update, va_tips_delete };
|
|
||||||
|
|
@ -234,7 +234,7 @@ async function removeEntityRelationships(data) {
|
||||||
}}
|
}}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove projectId from VAs
|
// Remove projectId from VAs
|
||||||
if (vaId && Array.isArray(vaId)) {
|
if (vaId && Array.isArray(vaId)) {
|
||||||
// await VAModel.updateMany(
|
// await VAModel.updateMany(
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ import logger from "../config/logger.js";
|
||||||
|
|
||||||
async function authValidator(req, res, next) {
|
async function authValidator(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
const sf_token = req.headers.sf_token;
|
||||||
if (sf_token) {
|
if (sf_token) {
|
||||||
const { vaId } = req.params;
|
|
||||||
if(vaId) req.body.vaId = vaId;
|
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
@ -50,15 +48,6 @@ async function authValidator(req, res, next) {
|
||||||
|
|
||||||
async function clientAuthValidator(req, res, next) {
|
async function clientAuthValidator(req, res, next) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const sf_token = req.headers["x-auth-token"];
|
|
||||||
if (sf_token) {
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(sf_token) return;
|
|
||||||
|
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,6 @@ const userDetailsSchema = new Schema({
|
||||||
veemAccount: {
|
veemAccount: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
wiseEmail: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
phone: {
|
phone: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -89,46 +86,19 @@ const projectSchema = new Schema({
|
||||||
},
|
},
|
||||||
vaAssigned: [
|
vaAssigned: [
|
||||||
{
|
{
|
||||||
_id: false,
|
type: Types.ObjectId,
|
||||||
vaId: {
|
ref: "VA",
|
||||||
type: Types.ObjectId,
|
},
|
||||||
ref: "VA",
|
|
||||||
},
|
|
||||||
isFlexibleAllowed: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
dedicatedHours: {
|
dedicatedHours: {
|
||||||
type: Array,
|
type: String,
|
||||||
default: ["0", "0", "0", "0", "0", "0", "0"]
|
default: "0"
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["ACTIVE", "INACTIVE"],
|
enum: ["ACTIVE", "INACTIVE"],
|
||||||
default: "ACTIVE"
|
default: "ACTIVE"
|
||||||
},
|
}
|
||||||
vaWorkingScheduleStart: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
vaWorkingScheduleEnd: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
isFlexibleAllowed: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
vaWorkingDelayAllowedStart: {
|
|
||||||
type: String,
|
|
||||||
default: '5m'
|
|
||||||
},
|
|
||||||
vaWorkingDelayAllowedEnd: {
|
|
||||||
type: String,
|
|
||||||
default: '5m'
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------------------- Client Details Schema ---------------------- */
|
/* ---------------------- Client Details Schema ---------------------- */
|
||||||
|
|
@ -136,12 +106,6 @@ const clientDetailsSchema = new Schema({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
firstName: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
lastName: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -202,7 +166,7 @@ const clientDetailsSchema = new Schema({
|
||||||
enum: ["CHECKING", "SAVINGS"],
|
enum: ["CHECKING", "SAVINGS"],
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
accountHolderName: {
|
accountHolderName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -350,6 +314,12 @@ const vaWorkTrackSchema = new Schema({
|
||||||
type: Types.ObjectId,
|
type: Types.ObjectId,
|
||||||
ref: "TrackLog",
|
ref: "TrackLog",
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
appName: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
from: {
|
from: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now,
|
default: Date.now,
|
||||||
|
|
@ -362,45 +332,41 @@ const vaWorkTrackSchema = new Schema({
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
logs: {
|
|
||||||
type: Array,
|
|
||||||
default: [],
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const passwordResetSchema = new mongoose.Schema({
|
const passwordResetSchema = new mongoose.Schema({
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
lowercase: true,
|
lowercase: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
userType: {
|
userType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
enum: ['va', 'client'],
|
enum: ['va', 'client'],
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
expiresAt: {
|
expiresAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true,
|
required: true,
|
||||||
index: true,
|
index: true,
|
||||||
expires: 3600 // TTL index for 1 hour
|
expires: 3600 // TTL index for 1 hour
|
||||||
},
|
},
|
||||||
used: {
|
used: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: true
|
timestamps: true
|
||||||
});
|
});
|
||||||
|
|
||||||
passwordResetSchema.index({ email: 1, used: 1 });
|
passwordResetSchema.index({ email: 1, used: 1 });
|
||||||
|
|
@ -435,14 +401,6 @@ const adjustedHoursSchema = new Schema({
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
attachments: [{
|
|
||||||
filename: String,
|
|
||||||
mimeType: String,
|
|
||||||
base64Data: String,
|
|
||||||
size: Number,
|
|
||||||
originalHTML: String,
|
|
||||||
index: Number
|
|
||||||
}],
|
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["PENDING", "APPROVED", "REJECTED"],
|
enum: ["PENDING", "APPROVED", "REJECTED"],
|
||||||
|
|
@ -492,14 +450,6 @@ const leavesSchema = new Schema({
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
attachments: [{
|
|
||||||
filename: String,
|
|
||||||
mimeType: String,
|
|
||||||
base64Data: String,
|
|
||||||
size: Number,
|
|
||||||
originalHTML: String,
|
|
||||||
index: Number
|
|
||||||
}],
|
|
||||||
leaveType: {
|
leaveType: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["SICK", "PTO"],
|
enum: ["SICK", "PTO"],
|
||||||
|
|
@ -519,37 +469,6 @@ const leavesSchema = new Schema({
|
||||||
}
|
}
|
||||||
}, { timestamps: true });
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------- Tips Schema ---------------------- */
|
|
||||||
const tipsSchema = new Schema({
|
|
||||||
clientId: {
|
|
||||||
type: Types.ObjectId,
|
|
||||||
ref: "Client"
|
|
||||||
},
|
|
||||||
vaId: {
|
|
||||||
type: Types.ObjectId,
|
|
||||||
ref: "VA"
|
|
||||||
},
|
|
||||||
vaName: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
enum: ["Unbilled", "Billed"],
|
|
||||||
default: "Unbilled",
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
}
|
|
||||||
}, { timestamps: true })
|
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------- Model Exports ---------------------- */
|
/* ---------------------- Model Exports ---------------------- */
|
||||||
const VAModel = model("VA", vaSchema);
|
const VAModel = model("VA", vaSchema);
|
||||||
const ClientModel = model("Client", clientSchema);
|
const ClientModel = model("Client", clientSchema);
|
||||||
|
|
@ -559,6 +478,5 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
|
||||||
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
|
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
|
||||||
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
|
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
|
||||||
const LeavesModel = model("Leaves", leavesSchema);
|
const LeavesModel = model("Leaves", leavesSchema);
|
||||||
const TipsModel = model("Tips", tipsSchema);
|
|
||||||
|
|
||||||
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel };
|
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };
|
||||||
|
|
@ -13,7 +13,7 @@ class VaClientRepository {
|
||||||
|
|
||||||
async findByEmail(email) {
|
async findByEmail(email) {
|
||||||
try {
|
try {
|
||||||
const response = await ClientModel.findOne({ "userDetails.email": email });
|
const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() });
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -31,27 +31,10 @@ class VaClientRepository {
|
||||||
|
|
||||||
async update(id, data) {
|
async update(id, data) {
|
||||||
try {
|
try {
|
||||||
// Convert nested userDetails object to dot notation
|
|
||||||
const updateObj = {};
|
|
||||||
|
|
||||||
// Handle userDetails fields with dot notation
|
|
||||||
if (data.userDetails) {
|
|
||||||
Object.keys(data.userDetails).forEach(key => {
|
|
||||||
updateObj[`userDetails.${key}`] = data.userDetails[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle other top-level fields if any
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
if (key !== 'userDetails') {
|
|
||||||
updateObj[key] = data[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await ClientModel.findByIdAndUpdate(
|
const response = await ClientModel.findByIdAndUpdate(
|
||||||
id,
|
id,
|
||||||
{ $set: updateObj },
|
{ $set: { ...data } },
|
||||||
{ new: true, runValidators: true }
|
{ new: true }
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -59,53 +42,9 @@ class VaClientRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findProfileByEmail(email) {
|
async delete(id) {
|
||||||
try {
|
try {
|
||||||
const response = await ClientModel.findOne(
|
const response = await ClientModel.findByIdAndDelete(id);
|
||||||
{ "userDetails.email": email },
|
|
||||||
{
|
|
||||||
// Only specify the fields you want to include
|
|
||||||
"userDetails.name": 1,
|
|
||||||
"userDetails.email": 1,
|
|
||||||
"userDetails.address": 1,
|
|
||||||
"userDetails.city": 1,
|
|
||||||
"userDetails.state": 1,
|
|
||||||
"userDetails.zip": 1,
|
|
||||||
"userDetails.position": 1
|
|
||||||
// Don't include any exclusion fields (0) when using inclusion (1)
|
|
||||||
}
|
|
||||||
).lean(); // Use lean() for better performance since we don't need mongoose document
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alternative: If you need to be more explicit about excluding sensitive fields
|
|
||||||
async findProfileByEmailExplicit(email) {
|
|
||||||
try {
|
|
||||||
const response = await ClientModel.findOne(
|
|
||||||
{ "userDetails.email": email }
|
|
||||||
).select({
|
|
||||||
'userDetails.name': 1,
|
|
||||||
'userDetails.email': 1,
|
|
||||||
'userDetails.address': 1,
|
|
||||||
'userDetails.city': 1,
|
|
||||||
'userDetails.state': 1,
|
|
||||||
'userDetails.zip': 1,
|
|
||||||
'userDetails.position': 1
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id, options = {}) {
|
|
||||||
try {
|
|
||||||
const response = await ClientModel.findByIdAndDelete(id, options);
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
const clientRoutes = express.Router();
|
const clientRoutes = express.Router();
|
||||||
|
|
||||||
import { va_register_client, delete_client, update_client, get_client_dashboard_profile, update_client_password } from "../../controllers/client/vaClient.controller.js";
|
import { va_register_client } from "../../controllers/client/vaClient.controller.js";
|
||||||
// import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.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_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
|
||||||
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
|
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
|
||||||
|
|
@ -11,11 +11,7 @@ import { clientAuthValidator } from "../../middlewares/auth.middleware.js";
|
||||||
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js";
|
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js";
|
||||||
import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js";
|
import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js";
|
||||||
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
|
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
|
||||||
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
|
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js"
|
||||||
import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js";
|
|
||||||
import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js";
|
|
||||||
import { clientDashboardGetVaLogsController } from "../../controllers/client/dashboard/clientDashboardGetVaLogs.controller.js";
|
|
||||||
import { va_credits_get } from "../../controllers/client/dashboard/clientDashboardCredits.controller.js";
|
|
||||||
|
|
||||||
// clients routes
|
// clients routes
|
||||||
clientRoutes.post("/register", va_register_client); // Register a new VA
|
clientRoutes.post("/register", va_register_client); // Register a new VA
|
||||||
|
|
@ -23,28 +19,16 @@ clientRoutes.post("/login", va_login_client); // Login Client
|
||||||
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
|
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
|
||||||
clientRoutes.post("/forgot-password", client_request_password_reset);
|
clientRoutes.post("/forgot-password", client_request_password_reset);
|
||||||
clientRoutes.post("/verify-client-email", client_verify_email);
|
clientRoutes.post("/verify-client-email", client_verify_email);
|
||||||
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
|
|
||||||
|
|
||||||
clientRoutes.post("/tips", clientAuthValidator, va_tips_manage_function);
|
|
||||||
clientRoutes.get("/tips", clientAuthValidator, va_tips_get);
|
|
||||||
clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update);
|
|
||||||
clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete);
|
|
||||||
|
|
||||||
clientRoutes.get("/credits", clientAuthValidator, va_credits_get);
|
|
||||||
|
|
||||||
clientRoutes.get("/vaLogsData/:vaId", clientAuthValidator, clientDashboardGetVaLogsController);
|
|
||||||
|
|
||||||
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
|
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
|
||||||
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// dashboard API's
|
// dashboard API's
|
||||||
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
|
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
|
||||||
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
|
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
|
||||||
clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback
|
clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback
|
||||||
clientRoutes.get("/profile", clientAuthValidator, get_client_dashboard_profile);
|
|
||||||
clientRoutes.patch("/profile", clientAuthValidator, update_client_password);
|
|
||||||
clientRoutes.patch("/:clientId", update_client);
|
|
||||||
clientRoutes.delete("/:clientId", delete_client);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,13 @@ import { process_reset } from "../../controllers/passwordReset.controllers.js";
|
||||||
import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js"
|
import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js"
|
||||||
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
|
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
|
||||||
import { authValidator } from "../../middlewares/auth.middleware.js";
|
import { authValidator } from "../../middlewares/auth.middleware.js";
|
||||||
import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js"
|
|
||||||
import { createWireGuardConnection, disconnectWireGuardConnection } from '../../vpnHandler/vpn.js';
|
|
||||||
|
|
||||||
const v1Routes = express.Router()
|
const v1Routes = express.Router()
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
v1Routes.get("/auth", function (req, res) {
|
v1Routes.get("/auth", function(req, res) {
|
||||||
res.sendFile(path.join(__dirname, '/auth.html'));
|
res.sendFile(path.join(__dirname, '/auth.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -31,54 +29,12 @@ v1Routes.use("/client", clientRoutes);
|
||||||
|
|
||||||
v1Routes.post("/project", authValidator, project_add_sf);
|
v1Routes.post("/project", authValidator, project_add_sf);
|
||||||
v1Routes.patch("/project/:projectId", authValidator, update_project_sf);
|
v1Routes.patch("/project/:projectId", authValidator, update_project_sf);
|
||||||
v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
|
|
||||||
|
|
||||||
v1Routes.post("/password/reset", process_reset);
|
v1Routes.post("/password/reset", process_reset);
|
||||||
|
|
||||||
v1Routes.get("/", function (req, res) {
|
v1Routes.get("/", function(req, res) {
|
||||||
res.json({ "connected": true })
|
res.json({"connected": true})
|
||||||
})
|
})
|
||||||
|
|
||||||
v1Routes.post("/vpn/connect", (req, res) => {
|
|
||||||
try {
|
|
||||||
const data = createWireGuardConnection();
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: "VPN connection created",
|
|
||||||
...data
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: err.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
v1Routes.post("/vpn/disconnect", (req, res) => {
|
|
||||||
try {
|
|
||||||
const { ip } = req.body;
|
|
||||||
if (!ip) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: "IP address is required"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = disconnectWireGuardConnection(ip);
|
|
||||||
if (result && result.success) {
|
|
||||||
return res.status(200).json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).json({success: false, error: result.message});
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: err.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// v1Routes.use("/form_submit", authValidator, formRoutes);
|
// v1Routes.use("/form_submit", authValidator, formRoutes);
|
||||||
export default v1Routes;
|
export default v1Routes;
|
||||||
|
|
@ -26,11 +26,8 @@ import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.contr
|
||||||
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js";
|
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js";
|
||||||
import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
|
import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
|
||||||
import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js";
|
import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js";
|
||||||
import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.controller.js";
|
|
||||||
import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js";
|
|
||||||
import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js";
|
|
||||||
|
|
||||||
vaRoutes.get("/", va_callback); // VA Callback for getting active VA list
|
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
|
||||||
|
|
||||||
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
|
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
|
||||||
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
|
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
|
||||||
|
|
@ -42,14 +39,14 @@ vaRoutes.post("/verify-va-email", va_verify_email);
|
||||||
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
|
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
|
||||||
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
|
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
|
||||||
|
|
||||||
vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
|
|
||||||
|
|
||||||
|
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
||||||
|
|
||||||
// timesheet adjustment routes
|
// timesheet adjustment routes
|
||||||
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
||||||
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
||||||
vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
// vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
||||||
|
|
||||||
|
|
||||||
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
||||||
|
|
||||||
// Leaves routes
|
// Leaves routes
|
||||||
|
|
@ -57,8 +54,8 @@ vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all l
|
||||||
vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status
|
vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status
|
||||||
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
|
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
|
||||||
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
|
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
|
||||||
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
|
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
|
||||||
vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function);
|
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
|
||||||
|
|
||||||
// timesheet adjustment routes
|
// timesheet adjustment routes
|
||||||
|
|
||||||
|
|
@ -68,7 +65,7 @@ vaRoutes.get("/search", va_filter_search);
|
||||||
|
|
||||||
|
|
||||||
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
|
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
|
||||||
vaRoutes.get("/report/project/:projectId/:from?/:to?/:vaId?", authValidator, va_projects_report_function);
|
vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
|
||||||
|
|
||||||
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
|
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
|
||||||
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
|
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
|
||||||
|
|
@ -76,17 +73,12 @@ vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_project
|
||||||
vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project
|
vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project
|
||||||
|
|
||||||
vaRoutes.post("/log/project/track", authValidator, va_log_track_work_callback); // Track Window Tabs Logs Data
|
vaRoutes.post("/log/project/track", authValidator, va_log_track_work_callback); // Track Window Tabs Logs Data
|
||||||
// vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time
|
vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time
|
||||||
vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); // VA Audit Report // All Projects
|
vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); // VA Audit Report // All Projects
|
||||||
vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report
|
vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report
|
||||||
// VA filter search: supports query params `name`, `email`, `from`, `to`
|
// VA filter search: supports query params `name`, `email`, `from`, `to`
|
||||||
|
|
||||||
|
|
||||||
// Payroll routes
|
|
||||||
vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule
|
|
||||||
vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip
|
|
||||||
|
|
||||||
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
|
||||||
|
|
||||||
|
|
||||||
export default vaRoutes;
|
export default vaRoutes;
|
||||||
397
src/server.js
397
src/server.js
|
|
@ -18,7 +18,6 @@ import apiRoutes from './routes/index.js'
|
||||||
|
|
||||||
|
|
||||||
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
|
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
|
||||||
import expressLayouts from "express-ejs-layouts";
|
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
@ -27,6 +26,19 @@ const app = express();
|
||||||
const server = new createServer(app);
|
const server = new createServer(app);
|
||||||
const io = new Server(server);
|
const io = new Server(server);
|
||||||
|
|
||||||
|
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||||
|
console.log("⚠️ Certificate error:", url);
|
||||||
|
|
||||||
|
// Allow for ngrok only
|
||||||
|
if (url.includes("ngrok")) {
|
||||||
|
event.preventDefault();
|
||||||
|
callback(true); // approve the certificate
|
||||||
|
} else {
|
||||||
|
callback(false); // reject others
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||||
console.log("⚠️ Certificate error:", url);
|
console.log("⚠️ Certificate error:", url);
|
||||||
|
|
||||||
|
|
@ -40,11 +52,6 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba
|
||||||
});
|
});
|
||||||
|
|
||||||
app.set("view engine", "ejs");
|
app.set("view engine", "ejs");
|
||||||
|
|
||||||
app.use(expressLayouts);
|
|
||||||
|
|
||||||
// DEFAULT layout for all pages
|
|
||||||
app.set("layout", "layouts/dashboard");
|
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
|
|
@ -65,9 +72,7 @@ app.use('/api', apiRoutes);
|
||||||
|
|
||||||
app.get("/download-tracker", (req, res) => {
|
app.get("/download-tracker", (req, res) => {
|
||||||
res.render("download_tracker", {
|
res.render("download_tracker", {
|
||||||
title: 'Download tracker',
|
version: "3.2.1"
|
||||||
version: "3.2.1",
|
|
||||||
layout: 'layouts/main'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -82,82 +87,53 @@ app.get("/download-file", (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/varegister/:sf_token?', (req, res) => {
|
app.get('/varegister/:sf_token?', (req, res) => {
|
||||||
const { sf_token } = req.params;
|
const { sf_token } = req.params;
|
||||||
|
|
||||||
// No token → invalid request
|
try {
|
||||||
if (!sf_token) {
|
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
||||||
return res.status(404).render('not_found', {
|
|
||||||
title: 'Page Not Found',
|
|
||||||
layout: 'layouts/error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (decoded) {
|
||||||
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
res.render('va_register', { title: "VA Registration" });
|
||||||
|
} else {
|
||||||
|
res.render('not_found', { title: "VA Registration" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.render('not_found', { title: "VA Registration" });
|
||||||
|
}
|
||||||
|
|
||||||
// Token valid
|
// if(verified) {
|
||||||
return res.render('va_register', {
|
// res.render('va_register', { title: "VA Registration" });
|
||||||
title: 'VA Registration',
|
// } else {
|
||||||
email: decoded.email,
|
// res.render('not_founf', { title: "VA Registration" });
|
||||||
layout: 'layouts/varegisterlayout'
|
// }
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Invalid / expired token
|
|
||||||
return res.status(401).render('not_found', {
|
|
||||||
title: 'Invalid or Expired Link',
|
|
||||||
layout: 'layouts/error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.get('/clientregister/:sf_token?', (req, res) => {
|
app.get('/clientregister/:sf_token?', (req, res) => {
|
||||||
const { sf_token } = req.params;
|
const { sf_token } = req.params;
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
||||||
|
|
||||||
// No token → invalid request
|
if (decoded) {
|
||||||
if (!sf_token) {
|
res.render('va_client_register', { title: "Client Registration" });
|
||||||
return res.status(404).render('not_found', {
|
} else {
|
||||||
title: 'Page Not Found',
|
res.render('not_found', { title: "Client Registration" });
|
||||||
layout: 'layouts/error'
|
}
|
||||||
});
|
} catch (error) {
|
||||||
}
|
res.render('not_found', { title: "Client Registration" });
|
||||||
|
}
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
|
||||||
|
|
||||||
// Token valid
|
|
||||||
return res.render('va_client_register', {
|
|
||||||
title: 'Client Registration',
|
|
||||||
email: decoded.email,
|
|
||||||
layout: 'layouts/vaclientlayout'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Invalid / expired token
|
|
||||||
return res.status(401).render('not_found', {
|
|
||||||
title: 'Invalid or Expired Link',
|
|
||||||
layout: 'layouts/error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/auth/password/reset-form', render_reset_form);
|
app.get('/auth/password/reset-form', render_reset_form);
|
||||||
|
|
||||||
app.get("/client/login", (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
// res.render("client_login")
|
res.status(201).send({ status: "Server is healthy" });
|
||||||
res.render("client/client_login", {
|
|
||||||
layout: "layouts/auth", // ⬅️ override layout
|
|
||||||
title: "DocVA | Login"
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get("/client/login", (req, res) => {
|
||||||
|
res.render("client_login")
|
||||||
|
})
|
||||||
app.get("/client/forgot-password", (req, res) => {
|
app.get("/client/forgot-password", (req, res) => {
|
||||||
|
res.render("forgot_password")
|
||||||
res.render("forgot_password", {
|
|
||||||
title: "Forgot Password",
|
|
||||||
layout: "layouts/forgotpasswordlayout"
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Client Dashboard Routes with new file names
|
// Client Dashboard Routes with new file names
|
||||||
|
|
@ -165,11 +141,7 @@ app.get('/client/dashboard', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// const dashboardData = await dashboardService.getClientDashboard(req.user.id);
|
// const dashboardData = await dashboardService.getClientDashboard(req.user.id);
|
||||||
// res.render('client/dashboard-dashboard', dashboardData);
|
// res.render('client/dashboard-dashboard', dashboardData);
|
||||||
// res.render('client/dashboard-dashboard');
|
res.render('client/dashboard-dashboard');
|
||||||
res.render('client/dashboard-dashboard', {
|
|
||||||
title: "Client Dashboard",
|
|
||||||
pageScript: "dashboard.js"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Dashboard error:', error);
|
console.error('Dashboard error:', error);
|
||||||
res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' });
|
res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' });
|
||||||
|
|
@ -179,12 +151,7 @@ app.get('/client/dashboard', async (req, res) => {
|
||||||
app.get('/client/virtual-assistants', async (req, res) => {
|
app.get('/client/virtual-assistants', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||||||
// res.render('client/dashboard-virtual-assistants');
|
res.render('client/dashboard-virtual-assistants');
|
||||||
res.render('client/dashboard-virtual-assistants', {
|
|
||||||
title: "Virtual Assistants",
|
|
||||||
pageScript:"dashboard-virtual-assistants.js"
|
|
||||||
|
|
||||||
});
|
|
||||||
// res.render('client/dashboard-virtual-assistants', vaData);
|
// res.render('client/dashboard-virtual-assistants', vaData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Virtual assistants error:', error);
|
console.error('Virtual assistants error:', error);
|
||||||
|
|
@ -195,78 +162,18 @@ app.get('/client/virtual-assistants', async (req, res) => {
|
||||||
app.get('/client/projects', async (req, res) => {
|
app.get('/client/projects', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||||||
// res.render('client/dashboard-projects');
|
res.render('client/dashboard-projects');
|
||||||
res.render('client/dashboard-projects', {
|
|
||||||
title: "Projects",
|
|
||||||
pageScript:"projects.js"
|
|
||||||
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Projects error:', error);
|
console.error('Projects error:', error);
|
||||||
res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' });
|
res.status(500).render('client/dashboard-projects', { error: 'Failed to load virtual assistants' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/client/profile', async (req, res) => {
|
|
||||||
try {
|
|
||||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
|
||||||
// res.render('client/dashboard-profile');
|
|
||||||
res.render('client/dashboard-profile',{
|
|
||||||
title: "Profile",
|
|
||||||
pageScript:"profile.js"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Projects error:', error);
|
|
||||||
res.status(500).render('client/dashboard-profile', { error: 'Failed to load profile' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/client/virtual-assistants/manage-tips', async (req, res) => {
|
|
||||||
try {
|
|
||||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
|
||||||
// res.render('client/dashboard-manage-va-tips');
|
|
||||||
res.render('client/dashboard-manage-va-tips', {
|
|
||||||
title: 'Manage VA Tips',
|
|
||||||
pageScript: 'manage-tips.js'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Projects error:', error);
|
|
||||||
res.status(500).render('client/dashboard-manage-va-tips', { error: 'Failed to load profile' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/client/virtual-assistants/view-logs', async (req, res) => {
|
|
||||||
try {
|
|
||||||
// res.render('client/dashboard-va-logs');
|
|
||||||
res.render('client/dashboard-va-logs', {
|
|
||||||
title: 'VA Logs',
|
|
||||||
pageScript: 'va-logs.js'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Projects error:', error);
|
|
||||||
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/client/credits', async (req, res) => {
|
|
||||||
try {
|
|
||||||
// res.render('client/dashboard-va-logs');
|
|
||||||
res.render('client/dashboard-credits', {
|
|
||||||
title: 'Credits',
|
|
||||||
pageScript: 'va-credits.js'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Projects error:', error);
|
|
||||||
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
app.get("/live/:projectId/:vaId", async (req, res) => {
|
app.get("/live/:projectId/:vaId", async (req, res) => {
|
||||||
const { projectId, vaId } = req.params;
|
const { projectId, vaId } = req.params;
|
||||||
|
|
||||||
if (!projectId || !vaId) {
|
if (!projectId || !vaId) {
|
||||||
return res.status(400).render("not_found", { title: "Page not found" });
|
return res.status(400).send({ "message": "Page not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let clientId = null;
|
let clientId = null;
|
||||||
|
|
@ -277,45 +184,36 @@ app.get("/live/:projectId/:vaId", async (req, res) => {
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching client for project:", err.message);
|
console.error("Error fetching client for project:", err.message);
|
||||||
return res.status(500).render("not_found", { title: "Server Error" });
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) return res.status(400).send({ "message": "Page not found" });
|
||||||
return res.status(400).render("not_found", { title: "Page not found" });
|
|
||||||
|
res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get("/live/:projectId/:vaId", async (req, res) => {
|
||||||
|
const { projectId, vaId } = req.params;
|
||||||
|
|
||||||
|
if (!projectId || !vaId) {
|
||||||
|
return res.status(400).send({ "message": "Page not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render("va_live_tracker", {
|
let clientId = null;
|
||||||
title: "VA Live Tracker",
|
try {
|
||||||
projectId,
|
const clientRes = await ProjectModel.findById(projectId);
|
||||||
vaId,
|
if (clientRes) {
|
||||||
clientId,
|
clientId = clientRes.clientId.toString();
|
||||||
layout: "layouts/valivetrackerlayout" // ✅ ADD THIS
|
}
|
||||||
});
|
} catch (err) {
|
||||||
});
|
console.error("Error fetching client for project:", err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientId) return res.status(400).send({ "message": "Page not found" });
|
||||||
|
|
||||||
// app.get("/live/:projectId/:vaId", async (req, res) => {
|
res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
|
||||||
// const { projectId, vaId } = req.params;
|
})
|
||||||
|
|
||||||
// if (!projectId || !vaId) {
|
|
||||||
// return res.status(400).send({ "message": "Page not found" });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let clientId = null;
|
|
||||||
// try {
|
|
||||||
// const clientRes = await ProjectModel.findById(projectId);
|
|
||||||
// if (clientRes) {
|
|
||||||
// clientId = clientRes.clientId.toString();
|
|
||||||
// }
|
|
||||||
// } catch (err) {
|
|
||||||
// console.error("Error fetching client for project:", err.message);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!clientId) return res.status(400).send({ "message": "Page not found" });
|
|
||||||
|
|
||||||
// res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// Handling Socket
|
// Handling Socket
|
||||||
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
|
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
|
||||||
|
|
@ -392,8 +290,6 @@ io.use(async (socket, next) => {
|
||||||
if (clientUser) {
|
if (clientUser) {
|
||||||
socket.data.role = "client";
|
socket.data.role = "client";
|
||||||
socket.data.clientId = clientUser._id.toString();
|
socket.data.clientId = clientUser._id.toString();
|
||||||
socket.data.vaId = vaId.toString();
|
|
||||||
socket.data.projectId = projectId;
|
|
||||||
|
|
||||||
// Get all VA IDs assigned to this client
|
// Get all VA IDs assigned to this client
|
||||||
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
|
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
|
||||||
|
|
@ -414,7 +310,7 @@ io.on("connection", (socket) => {
|
||||||
console.log("User connected:", socket.id);
|
console.log("User connected:", socket.id);
|
||||||
|
|
||||||
// Register role and ID (client or VA)
|
// Register role and ID (client or VA)
|
||||||
socket.on("register", async ({ role, id, projectId = null, clientId = null }) => {
|
socket.on("register", async ({ role, id }) => {
|
||||||
if (role === "va") {
|
if (role === "va") {
|
||||||
socket.data.role = "va";
|
socket.data.role = "va";
|
||||||
socket.data.vaId = id;
|
socket.data.vaId = id;
|
||||||
|
|
@ -423,35 +319,23 @@ io.on("connection", (socket) => {
|
||||||
|
|
||||||
if (role === "client") {
|
if (role === "client") {
|
||||||
socket.data.role = "client";
|
socket.data.role = "client";
|
||||||
socket.data.clientId = clientId;
|
socket.data.clientId = id;
|
||||||
|
|
||||||
// 🧩 Get allowed VAs for this client
|
// 🧩 Get allowed VAs for this client
|
||||||
const client = await ClientModel.findById(clientId);
|
const client = await ClientModel.findById(id)
|
||||||
|
.populate("projectIds") // to access their projects
|
||||||
const a = JSON.stringify(client);
|
.populate("vaIds"); // optional if you track directly
|
||||||
console.log(a, "client data for allowed VAs");
|
|
||||||
if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
|
|
||||||
const projectVAIds = new Set();
|
|
||||||
|
|
||||||
const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
|
|
||||||
populated.vaAssigned.forEach((va) => projectVAIds.add(va.vaId.toString()));
|
|
||||||
|
|
||||||
socket.data.allowedVAs = Array.from(projectVAIds);
|
|
||||||
console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
|
|
||||||
|
|
||||||
io.to(vaSocketRespectToClient.get(`${clientId}-${id}-${projectId}-va`)).emit("startSendAppsDetails", { projId: projectId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all VA IDs from their projects
|
// Collect all VA IDs from their projects
|
||||||
// const projectVAIds = new Set();
|
const projectVAIds = new Set();
|
||||||
// for (const project of client.projectIds) {
|
for (const project of client.projectIds) {
|
||||||
// const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
|
const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
|
||||||
// populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
|
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Store allowed VA IDs in socket memory
|
// Store allowed VA IDs in socket memory
|
||||||
// socket.data.allowedVAs = Array.from(projectVAIds);
|
socket.data.allowedVAs = Array.from(projectVAIds);
|
||||||
// console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
|
console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -507,7 +391,7 @@ io.on("connection", (socket) => {
|
||||||
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
|
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("requestVisibleApps", async ({ token }) => {
|
socket.on("getVisibleApps", async ({ token, apps }) => {
|
||||||
try {
|
try {
|
||||||
// Verify token
|
// Verify token
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
|
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||||
|
|
@ -523,35 +407,6 @@ io.on("connection", (socket) => {
|
||||||
const vaId = vaUser._id.toString();
|
const vaId = vaUser._id.toString();
|
||||||
console.log("🟢 Received Apps from VA:", vaId);
|
console.log("🟢 Received Apps from VA:", vaId);
|
||||||
|
|
||||||
// Broadcast image only to allowed clients
|
|
||||||
for (let [_, clientSocket] of io.sockets.sockets) {
|
|
||||||
if (
|
|
||||||
clientSocket.data.role === "client" &&
|
|
||||||
clientSocket.data.allowedVAs?.includes(vaId)
|
|
||||||
) {
|
|
||||||
clientSocket.emit("captureAppsTracker");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Token verification failed:", err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("getVisibleApps", async ({ token, apps }) => {
|
|
||||||
try {
|
|
||||||
// Verify token
|
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
|
|
||||||
const vaEmail = decoded.email;
|
|
||||||
|
|
||||||
// Find VA in DB
|
|
||||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
|
||||||
if (!vaUser) {
|
|
||||||
console.log("❌ Unauthorized VA");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vaId = vaUser._id.toString();
|
|
||||||
|
|
||||||
// Broadcast image only to allowed clients
|
// Broadcast image only to allowed clients
|
||||||
for (let [_, clientSocket] of io.sockets.sockets) {
|
for (let [_, clientSocket] of io.sockets.sockets) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -567,77 +422,15 @@ io.on("connection", (socket) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
// console.log('a user Disconnected');
|
console.log('a user Disconnected');
|
||||||
// });
|
|
||||||
|
|
||||||
// When client leaves or closes tab
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
if (socket.data.role === "client") {
|
|
||||||
const { clientId, vaId, projectId } = socket.data;
|
|
||||||
|
|
||||||
const vaSocketId = vaSocketRespectToClient.get(
|
|
||||||
`${clientId}-${vaId}-${projectId}-va`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (vaSocketId) {
|
|
||||||
// io.to(vaSocketId).emit("stopSendAppsDetails");
|
|
||||||
io.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("stopSendAppsDetails", { projId: projectId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/health", (req, res) => {
|
|
||||||
return res.status(200).json({ "success": true, "message": "Server is Healthy" })
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post("/post-test", (req, res) => {
|
|
||||||
console.log("✅ POST request received at /post-test:", req.body);
|
|
||||||
return res.status(200).json({ "success": true, "message": "POST request received", "data": req.body })
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/tnyk/:sf_token", (req, res) => {
|
|
||||||
const sf_token = req.params.sf_token;
|
|
||||||
console.log("Received token:", sf_token);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
|
||||||
if (decoded) {
|
|
||||||
res.render('client/dashboard-dashboard', { sf_token, title: "Client" });
|
|
||||||
} else {
|
|
||||||
res.render('not_found', { title: "Client" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
res.render('not_found', { title: "Client" });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
|
||||||
// res.render("client_login")
|
|
||||||
res.render("client/client_login", {
|
|
||||||
layout: "layouts/auth", // ⬅️ override layout
|
|
||||||
title: "DocVA | Login"
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("/forgot-password", (req, res) => {
|
|
||||||
res.render("forgot_password", {
|
|
||||||
title: "Forgot Password",
|
|
||||||
layout: "layouts/forgotpasswordlayout"
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.status(404).render("not_found", {
|
res.status(404).render("not_found", {
|
||||||
layout: "layouts/error",
|
title: "404 - Page Not Found"
|
||||||
title: "404 - Page Not Found"
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,25 @@ import jwt from "jsonwebtoken";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { VAModel, ClientModel } from "../models/vaModel.js";
|
import { VAModel, ClientModel } from "../models/vaModel.js";
|
||||||
import logger from "../config/logger.js";
|
import logger from "../config/logger.js";
|
||||||
import axios from "axios";
|
|
||||||
import authFetch from "../utils/authFetch.js";
|
|
||||||
|
|
||||||
class PasswordResetService {
|
class PasswordResetService {
|
||||||
constructor(passwordResetRepository) {
|
constructor(passwordResetRepository, emailService) {
|
||||||
this.passwordResetRepository = passwordResetRepository;
|
this.passwordResetRepository = passwordResetRepository;
|
||||||
// this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestPasswordReset(email, userType) {
|
async requestPasswordReset(email, userType) {
|
||||||
try {
|
try {
|
||||||
// Determine which model to use
|
// Determine which model to use
|
||||||
const userModel = userType === 'va' ? VAModel : ClientModel;
|
const userModel = userType === 'va' ? VAModel : ClientModel;
|
||||||
|
|
||||||
// Check if user exists
|
// Check if user exists
|
||||||
const user = await userModel.findOne({ "userDetails.email": email.toLowerCase() });
|
const user = await userModel.findOne({ "userDetails.email": email.toLowerCase() });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.info(`Password reset requested for non-existent ${userType}: ${email}`);
|
logger.info(`Password reset requested for non-existent ${userType}: ${email}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Email not recognized. Please sign up to continue."
|
message: "Email not recognized. Please sign up to continue."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +29,7 @@ class PasswordResetService {
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
userType: userType,
|
userType: userType,
|
||||||
type: 'password_reset'
|
type: 'password_reset'
|
||||||
|
|
@ -62,9 +60,9 @@ class PasswordResetService {
|
||||||
await this.sendResetEmail(email, resetLink, userType);
|
await this.sendResetEmail(email, resetLink, userType);
|
||||||
|
|
||||||
logger.info(`Password reset token created for ${userType}: ${email}`);
|
logger.info(`Password reset token created for ${userType}: ${email}`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Email sent successfully. Please check your inbox."
|
message: "If the email exists, a reset link has been sent"
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -94,10 +92,10 @@ class PasswordResetService {
|
||||||
return { valid: false, message: "Invalid or expired token" };
|
return { valid: false, message: "Invalid or expired token" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
email: decoded.email,
|
email: decoded.email,
|
||||||
userType: decoded.userType
|
userType: decoded.userType
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -131,7 +129,7 @@ class PasswordResetService {
|
||||||
const userModel = userType === 'va' ? VAModel : ClientModel;
|
const userModel = userType === 'va' ? VAModel : ClientModel;
|
||||||
|
|
||||||
// Check if user exists and has matching token
|
// Check if user exists and has matching token
|
||||||
const user = await userModel.findOne({
|
const user = await userModel.findOne({
|
||||||
"userDetails.email": decoded.email,
|
"userDetails.email": decoded.email,
|
||||||
passwordResetToken: token
|
passwordResetToken: token
|
||||||
});
|
});
|
||||||
|
|
@ -152,12 +150,12 @@ class PasswordResetService {
|
||||||
await resetRecord.save();
|
await resetRecord.save();
|
||||||
|
|
||||||
// Send confirmation email
|
// Send confirmation email
|
||||||
// await this.sendPasswordChangedEmail(decoded.email, userType);
|
await this.sendPasswordChangedEmail(decoded.email, userType);
|
||||||
|
|
||||||
logger.info(`Password successfully reset for ${userType}: ${decoded.email}`);
|
logger.info(`Password successfully reset for ${userType}: ${decoded.email}`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Password has been reset successfully"
|
message: "Password has been reset successfully"
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -168,21 +166,17 @@ class PasswordResetService {
|
||||||
|
|
||||||
async sendResetEmail(email, resetLink, userType) {
|
async sendResetEmail(email, resetLink, userType) {
|
||||||
try {
|
try {
|
||||||
const userTypeText = userType === 'va' ? 'VA' : 'CLIENT';
|
const userTypeText = userType === 'va' ? 'Virtual Assistant' : 'Client';
|
||||||
|
|
||||||
|
const emailData = {
|
||||||
|
to: email,
|
||||||
|
subject: `${userTypeText} - Password Reset Request`,
|
||||||
|
html: this.generateResetEmailTemplate(resetLink, userTypeText),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.emailService.sendEmail(emailData);
|
||||||
|
logger.info(`Password reset email sent to ${userType}: ${email}`);
|
||||||
|
|
||||||
const res = await authFetch(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
data: {
|
|
||||||
email: email,
|
|
||||||
url: resetLink,
|
|
||||||
userType: userTypeText
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`);
|
logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`);
|
||||||
throw new Error("Failed to send reset email");
|
throw new Error("Failed to send reset email");
|
||||||
|
|
@ -192,7 +186,7 @@ class PasswordResetService {
|
||||||
async sendPasswordChangedEmail(email, userType) {
|
async sendPasswordChangedEmail(email, userType) {
|
||||||
try {
|
try {
|
||||||
const userTypeText = userType === 'va' ? 'Virtual Assistant' : 'Client';
|
const userTypeText = userType === 'va' ? 'Virtual Assistant' : 'Client';
|
||||||
|
|
||||||
const emailData = {
|
const emailData = {
|
||||||
to: email,
|
to: email,
|
||||||
subject: `${userTypeText} - Password Changed Successfully`,
|
subject: `${userTypeText} - Password Changed Successfully`,
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,29 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import logger from "../config/logger.js";
|
import logger from "../config/logger.js";
|
||||||
import authFetch from "../utils/authFetch.js";
|
|
||||||
|
|
||||||
class SalesforceService {
|
class SalesforceService {
|
||||||
|
|
||||||
async registerClientInSalesforce(clientData, _id) {
|
async registerClientInSalesforce(clientData, _id) {
|
||||||
try {
|
try {
|
||||||
const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`;
|
const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`;
|
||||||
|
|
||||||
// Extract only the required data for Salesforce
|
// Extract only the required data for Salesforce
|
||||||
const salesforcePayload = this.prepareSalesforcePayload(clientData, _id);
|
const salesforcePayload = this.prepareSalesforcePayload(clientData, _id);
|
||||||
|
|
||||||
logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`);
|
logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`);
|
||||||
|
|
||||||
const response = await authFetch(url, {
|
const response = await axios.post(url, salesforcePayload, {
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(salesforcePayload),
|
|
||||||
timeout: 30000, // 30 seconds timeout
|
timeout: 30000, // 30 seconds timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`SalesforceService - Client registered successfully in Salesforce: ${clientData.userDetails.email}`);
|
|
||||||
|
|
||||||
|
logger.info(`SalesforceService - Client registered successfully in Salesforce: ${clientData.userDetails.email}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`);
|
logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`);
|
||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// Salesforce returned an error response
|
// Salesforce returned an error response
|
||||||
logger.error(`SalesforceService - Salesforce API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
|
logger.error(`SalesforceService - Salesforce API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
|
||||||
|
|
@ -43,13 +41,11 @@ class SalesforceService {
|
||||||
|
|
||||||
prepareSalesforcePayload(clientData, _id) {
|
prepareSalesforcePayload(clientData, _id) {
|
||||||
const { userDetails } = clientData;
|
const { userDetails } = clientData;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userDetails: {
|
userDetails: {
|
||||||
_id: _id,
|
_id: _id,
|
||||||
name: userDetails.name,
|
name: userDetails.name,
|
||||||
firstName: userDetails.firstName,
|
|
||||||
lastName: userDetails.lastName,
|
|
||||||
email: userDetails.email.toLowerCase(),
|
email: userDetails.email.toLowerCase(),
|
||||||
address: userDetails.address || null,
|
address: userDetails.address || null,
|
||||||
city: userDetails.city || null,
|
city: userDetails.city || null,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import logger from "../config/logger.js";
|
import logger from "../config/logger.js";
|
||||||
import SalesforceService from "./salesforce.service.js";
|
import SalesforceService from "./salesforce.service.js";
|
||||||
import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
|
|
||||||
class VaClientService {
|
class VaClientService {
|
||||||
constructor(vaClientRepository) {
|
constructor(vaClientRepository) {
|
||||||
|
|
@ -18,7 +16,7 @@ class VaClientService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize and prepare data
|
// Sanitize and prepare data
|
||||||
const clientData = await this.sanitizeClientData(data);
|
const clientData = this.sanitizeClientData(data);
|
||||||
const response = await this.clientRepository.create(clientData);
|
const response = await this.clientRepository.create(clientData);
|
||||||
|
|
||||||
if(response) {
|
if(response) {
|
||||||
|
|
@ -55,227 +53,34 @@ class VaClientService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateClient(id, data) {
|
async updateClient(id, data) {
|
||||||
try {
|
try {
|
||||||
// Check if client exists
|
const response = await this.clientRepository.update(id, data);
|
||||||
const existingClient = await this.clientRepository.findById(id);
|
if (!response) throw new Error("Client not found");
|
||||||
if (!existingClient) {
|
return response;
|
||||||
throw new Error("Client not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { userDetails } = data;
|
|
||||||
|
|
||||||
if (!userDetails) {
|
|
||||||
throw new Error("No user details provided for update");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare update data
|
|
||||||
const updateData = {
|
|
||||||
userDetails: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define allowed fields for update (excluding password)
|
|
||||||
const allowedFields = [
|
|
||||||
"name", "firstName", "lastName", "address", "city", "state", "zip",
|
|
||||||
"billingName", "billingEmail", "billingAddress", "billingCity",
|
|
||||||
"billingState", "billingZip", "position", "signatoryName",
|
|
||||||
"signatoryEmail", "paymentMethod", "bankName", "accountNumber",
|
|
||||||
"routingNumber", "accountType", "accountHolderName", "cardNumber",
|
|
||||||
"expiryDate", "securityCode", "cardZip"
|
|
||||||
];
|
|
||||||
|
|
||||||
let hasUpdates = false;
|
|
||||||
const updatedFields = [];
|
|
||||||
|
|
||||||
// Process allowed fields
|
|
||||||
allowedFields.forEach(field => {
|
|
||||||
if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') {
|
|
||||||
updateData.userDetails[field] = userDetails[field];
|
|
||||||
hasUpdates = true;
|
|
||||||
updatedFields.push(field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle email update separately (needs uniqueness check)
|
|
||||||
if (userDetails.email && userDetails.email !== existingClient.userDetails.email) {
|
|
||||||
// Check if new email already exists
|
|
||||||
const emailExists = await this.clientRepository.findByEmail(userDetails.email);
|
|
||||||
if (emailExists && emailExists._id.toString() !== id) {
|
|
||||||
throw new Error("Email already exists");
|
|
||||||
}
|
|
||||||
updateData.userDetails.email = userDetails.email.toLowerCase();
|
|
||||||
hasUpdates = true;
|
|
||||||
updatedFields.push('email');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any fields were actually updated
|
|
||||||
if (!hasUpdates) {
|
|
||||||
throw new Error("No valid fields to update");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the client
|
|
||||||
const updatedClient = await this.clientRepository.update(id, updateData);
|
|
||||||
|
|
||||||
// Notify Salesforce about the update
|
|
||||||
try {
|
|
||||||
await this.salesforceService.updateClientInSalesforce(id, data);
|
|
||||||
logger.info(`Salesforce notified about client update: ${id}`);
|
|
||||||
} catch (sfError) {
|
|
||||||
logger.warn(`Salesforce notification failed for client update: ${sfError.message}`);
|
|
||||||
// Don't throw error - client update should proceed even if Salesforce fails
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Client updated successfully: ${id}, Fields: ${updatedFields.join(', ')}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...updatedClient.toObject(),
|
|
||||||
updateSummary: {
|
|
||||||
updatedFields
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Client Service - updateClient error: " + error.message);
|
logger.error("Client Service - updateClient error: " + error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteClient(clientId) {
|
async deleteClient(id) {
|
||||||
try {
|
try {
|
||||||
// Check if client exists
|
const response = await this.clientRepository.delete(id);
|
||||||
const existingClient = await this.clientRepository.findById(clientId);
|
return response;
|
||||||
if (!existingClient) {
|
|
||||||
throw new Error("Client not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientName = existingClient.userDetails?.name;
|
|
||||||
const vaIds = existingClient.vaIds || [];
|
|
||||||
const projectIds = existingClient.projectIds || [];
|
|
||||||
|
|
||||||
logger.info(`Deleting client: ${clientName} (${clientId})`);
|
|
||||||
|
|
||||||
const deletionSummary = {};
|
|
||||||
|
|
||||||
// 1. Remove client from all VAs
|
|
||||||
if (vaIds.length > 0) {
|
|
||||||
const vaResult = await VAModel.updateMany(
|
|
||||||
{ _id: { $in: vaIds } },
|
|
||||||
{ $pull: { clientIds: clientId } }
|
|
||||||
);
|
|
||||||
deletionSummary.vasUpdated = vaResult.modifiedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Delete all projects and associated data
|
|
||||||
if (projectIds.length > 0) {
|
|
||||||
// Remove project references from VAs
|
|
||||||
await VAModel.updateMany(
|
|
||||||
{ projectIds: { $in: projectIds } },
|
|
||||||
{ $pull: { projectIds: { $in: projectIds } } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete work tracks and logs
|
|
||||||
await WorkTrackModel.deleteMany({ projectId: { $in: projectIds } });
|
|
||||||
await LoggedModel.deleteMany({ projectId: { $in: projectIds } });
|
|
||||||
|
|
||||||
// Delete projects
|
|
||||||
const projectResult = await ProjectModel.deleteMany({ _id: { $in: projectIds } });
|
|
||||||
deletionSummary.projectsDeleted = projectResult.deletedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Delete the client
|
|
||||||
const deletedClient = await this.clientRepository.delete(clientId);
|
|
||||||
|
|
||||||
|
|
||||||
logger.info(`✅ Successfully deleted client: ${clientName} (${clientId})`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...deletedClient.toObject(),
|
|
||||||
deletionSummary
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Client Service - deleteClient error: " + error.message);
|
logger.error("Client Service - deleteClient error: " + error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClientProfileByEmail(email) {
|
|
||||||
try {
|
|
||||||
const response = await this.clientRepository.findProfileByEmail(email);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Client Service - getClientProfileByEmail error: " + error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateClientPassword(id, passwordData) {
|
|
||||||
try {
|
|
||||||
// Check if client exists
|
|
||||||
const existingClient = await this.clientRepository.findById(id);
|
|
||||||
if (!existingClient) {
|
|
||||||
throw new Error("Client not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentPassword, newPassword, confirmPassword } = passwordData;
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
|
||||||
throw new Error("All password fields are required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate password match
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
throw new Error("New passwords do not match");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify current password
|
|
||||||
const isMatch = await bcrypt.compare(currentPassword, existingClient.userDetails.password);
|
|
||||||
if (!isMatch) {
|
|
||||||
throw new Error("Current password is incorrect");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash new password
|
|
||||||
const saltRounds = 10;
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
|
||||||
|
|
||||||
// Prepare update data
|
|
||||||
const updateData = {
|
|
||||||
userDetails: {
|
|
||||||
password: hashedPassword
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the client password
|
|
||||||
const updatedClient = await this.clientRepository.update(id, updateData);
|
|
||||||
|
|
||||||
logger.info(`Client password updated successfully: ${id}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Password updated successfully",
|
|
||||||
clientId: id
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Client Service - updateClientPassword error: " + error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to sanitize data
|
// Helper method to sanitize data
|
||||||
async sanitizeClientData(payload) {
|
sanitizeClientData(payload) {
|
||||||
const { userDetails } = payload;
|
const { userDetails } = payload;
|
||||||
|
|
||||||
const saltRounds = 10;
|
|
||||||
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userDetails: {
|
userDetails: {
|
||||||
firstName: userDetails.firstName,
|
|
||||||
lastName: userDetails.lastName,
|
|
||||||
name: userDetails.name,
|
name: userDetails.name,
|
||||||
email: userDetails.email.toLowerCase(),
|
email: userDetails.email.toLowerCase(),
|
||||||
password: hashedPassword,
|
|
||||||
address: userDetails.address || null,
|
address: userDetails.address || null,
|
||||||
city: userDetails.city || null,
|
city: userDetails.city || null,
|
||||||
state: userDetails.state || null,
|
state: userDetails.state || null,
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import fetch from "node-fetch"; // or global fetch in Node 18+
|
|
||||||
|
|
||||||
let preOptions = {
|
|
||||||
token: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const getAccessToken = async () => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
|
|
||||||
const response = await fetch(`${process.env.SF_DOCTRACKER_API_URL}services/oauth2/token`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
grant_type: 'client_credentials',
|
|
||||||
client_id: process.env.SF_CLIENT_ID,
|
|
||||||
client_secret: process.env.SF_CLIENT_SECRET,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
console.log(response)
|
|
||||||
const access_token = (await response.json()).access_token;
|
|
||||||
preOptions.token = access_token;
|
|
||||||
return access_token;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching access token:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export const authFetch = async (url, options = {}) => {
|
|
||||||
let isvalid = false;
|
|
||||||
|
|
||||||
if (preOptions.token != "" && preOptions.token != null && preOptions.token != undefined) {
|
|
||||||
isvalid = await fetch(`${process.env.SF_DOCTRACKER_TOKEN_VALIDATION_URL}services/apexrest/docva/health}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${preOptions.token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const a = JSON.stringify(isvalid.body);
|
|
||||||
console.log(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isvalid.status !== 200) {
|
|
||||||
await getAccessToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...(options.headers || {}),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${preOptions.token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default authFetch;
|
|
||||||
export { getAccessToken }
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
const convertToObjectId = (id) => {
|
|
||||||
return mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default convertToObjectId;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
function getDayNumber(date = new Date()) {
|
|
||||||
const today = new Date(date);
|
|
||||||
return today.getUTCDay(); // getDay() already returns 0-6 for Sunday-Saturday
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getDayNumber;
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
import fs from "fs";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
const WG_CONF = "/etc/wireguard/wg0.conf";
|
|
||||||
const PEER_DB = "/etc/wireguard/peers.json";
|
|
||||||
const CLIENT_DIR = "/etc/wireguard/clients";
|
|
||||||
|
|
||||||
const SERVER_PUBLIC_KEY = process.env.SERVER_PUBLIC_KEY || "jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U=";
|
|
||||||
const SERVER_IP = process.env.SERVER_PUBLIC_IP || "35.95.209.161";
|
|
||||||
|
|
||||||
function loadDb() {
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(PEER_DB, "utf8"));
|
|
||||||
} catch {
|
|
||||||
const initial = { lastIp: 1, active: {}, free: [] };
|
|
||||||
fs.writeFileSync(PEER_DB, JSON.stringify(initial, null, 2));
|
|
||||||
return initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveDb(db) {
|
|
||||||
fs.writeFileSync(PEER_DB, JSON.stringify(db, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function allocateIp(db) {
|
|
||||||
if (db.free.length > 0) {
|
|
||||||
return db.free.shift(); // reuse old IP
|
|
||||||
}
|
|
||||||
|
|
||||||
db.lastIp++;
|
|
||||||
if (db.lastIp >= 255) db.lastIp = 2;
|
|
||||||
|
|
||||||
return `10.0.0.${db.lastIp}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateKeyPair() {
|
|
||||||
const privateKey = execSync("wg genkey").toString().trim();
|
|
||||||
const publicKey = execSync(`echo ${privateKey} | wg pubkey`).toString().trim();
|
|
||||||
return { privateKey, publicKey };
|
|
||||||
}
|
|
||||||
|
|
||||||
// function addPeerToServer(publicKey, ip) {
|
|
||||||
// const ipLast = ip.split(".")[3];
|
|
||||||
|
|
||||||
// const peerEntry = `
|
|
||||||
// [Peer]
|
|
||||||
// PublicKey = ${publicKey}
|
|
||||||
// AllowedIPs = 10.0.0.${ipLast}/32
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// fs.appendFileSync(WG_CONF, peerEntry);
|
|
||||||
|
|
||||||
// // apply config instantly
|
|
||||||
// // execSync(`wg syncconf wg0 <(wg-quick strip wg0)`, { shell: "/bin/bash" });
|
|
||||||
// execSync(`wg-quick strip wg0 > /tmp/wg0.tmp`);
|
|
||||||
// execSync(`wg syncconf wg0 /tmp/wg0.tmp`);
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
function addPeerToServer(publicKey, ip) {
|
|
||||||
const peerEntry = `
|
|
||||||
[Peer]
|
|
||||||
PublicKey = ${publicKey}
|
|
||||||
AllowedIPs = ${ip}/32
|
|
||||||
`;
|
|
||||||
|
|
||||||
fs.appendFileSync(WG_CONF, peerEntry);
|
|
||||||
|
|
||||||
execSync("wg-quick strip wg0 > /tmp/wg0.tmp");
|
|
||||||
execSync("wg syncconf wg0 /tmp/wg0.tmp");
|
|
||||||
execSync("rm -f /tmp/wg0.tmp");
|
|
||||||
|
|
||||||
execSync(
|
|
||||||
`wg set wg0 peer ${publicKey} allowed-ips ${ip}/32`,
|
|
||||||
{ stdio: "inherit" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function generateClientConf(privateKey, ip) {
|
|
||||||
return `
|
|
||||||
[Interface]
|
|
||||||
PrivateKey = ${privateKey}
|
|
||||||
Address = ${ip}/32
|
|
||||||
DNS = 1.1.1.1
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = ${SERVER_PUBLIC_KEY}
|
|
||||||
Endpoint = ${SERVER_IP}:51820
|
|
||||||
AllowedIPs = 10.0.0.0/24
|
|
||||||
PersistentKeepalive = 25
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createWireGuardConnection() {
|
|
||||||
let db = loadDb();
|
|
||||||
|
|
||||||
// 1. allocate IP
|
|
||||||
const ip = allocateIp(db);
|
|
||||||
|
|
||||||
// 2. generate keys
|
|
||||||
const { privateKey, publicKey } = generateKeyPair();
|
|
||||||
|
|
||||||
// 3. update server config
|
|
||||||
addPeerToServer(publicKey, ip);
|
|
||||||
|
|
||||||
// 4. update DB
|
|
||||||
db.active[ip] = publicKey;
|
|
||||||
saveDb(db);
|
|
||||||
|
|
||||||
// 5. prepare client.conf
|
|
||||||
const clientConf = generateClientConf(privateKey, ip);
|
|
||||||
|
|
||||||
// (optional) save it
|
|
||||||
if (!fs.existsSync(CLIENT_DIR)) fs.mkdirSync(CLIENT_DIR);
|
|
||||||
fs.writeFileSync(`${CLIENT_DIR}/client-${ip}.conf`, clientConf);
|
|
||||||
|
|
||||||
// 6. return to Electron app
|
|
||||||
return { ip, publicKey, clientConf };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// -------- Disconnect VPN PART START -----------
|
|
||||||
const removePeerFromConf = (publicKey) => {
|
|
||||||
const conf = fs.readFileSync(WG_CONF, "utf8");
|
|
||||||
|
|
||||||
const peerRegex = new RegExp(
|
|
||||||
`\\n\\[Peer\\][\\s\\S]*?PublicKey = ${publicKey}[\\s\\S]*?(?=\\n\\[Peer\\]|$)`,
|
|
||||||
"g"
|
|
||||||
);
|
|
||||||
|
|
||||||
const updated = conf.replace(peerRegex, "\n");
|
|
||||||
|
|
||||||
fs.writeFileSync(WG_CONF, updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncWireGuard() {
|
|
||||||
execSync("wg-quick strip wg0 > /tmp/wg0.tmp");
|
|
||||||
execSync("wg syncconf wg0 /tmp/wg0.tmp");
|
|
||||||
execSync("rm -f /tmp/wg0.tmp");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function disconnectWireGuardConnection(ip) {
|
|
||||||
const db = loadDb();
|
|
||||||
|
|
||||||
const publicKey = db.active[ip];
|
|
||||||
if (!db.active[ip]) {
|
|
||||||
return { success: false, message: "VPN already disconnected" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1️⃣ Remove peer from live kernel
|
|
||||||
execSync(`wg set wg0 peer ${publicKey} remove`);
|
|
||||||
|
|
||||||
// 2️⃣ Remove from config file
|
|
||||||
removePeerFromConf(publicKey);
|
|
||||||
|
|
||||||
// 3️⃣ Sync config
|
|
||||||
syncWireGuard();
|
|
||||||
|
|
||||||
// 4️⃣ Update DB (free IP)
|
|
||||||
delete db.active[ip];
|
|
||||||
db.free.push(ip);
|
|
||||||
saveDb(db);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "VPN disconnected",
|
|
||||||
ip
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
if (userType === 'client') {
|
if (userType === 'client') {
|
||||||
forgotPasswordUrl = '/client/forgot-password';
|
forgotPasswordUrl = '/client/forgot-password';
|
||||||
loginUrl = '/';
|
loginUrl = '/client/login';
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
|
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<%
|
<%
|
||||||
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
|
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
|
||||||
if (userType === 'client') {
|
if (userType === 'client') {
|
||||||
backToLoginUrl = '/';
|
backToLoginUrl = '/client/login';
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>
|
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<%
|
<%
|
||||||
let loginUrl = `${process.env.FRONTEND_URL}/login`;
|
let loginUrl = `${process.env.FRONTEND_URL}/login`;
|
||||||
if (userType === 'client') {
|
if (userType === 'client') {
|
||||||
loginUrl = '/';
|
loginUrl = '/client/login';
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a>
|
<a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a>
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<!-- Stats Cards -->
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-info">
|
|
||||||
<h3>Total Credits</h3>
|
|
||||||
<div class="stat-value" id="vaTotalCredits">$0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-icon blue">
|
|
||||||
<i class="fas fa-dollar fa-xl"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-info">
|
|
||||||
<h3>Available Credits</h3>
|
|
||||||
<div class="stat-value" id="vaAvailableCredits">$0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-icon blue">
|
|
||||||
<i class="fas fa-dollar fa-xl"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Virtual Assistants List -->
|
|
||||||
<div class="content-section">
|
|
||||||
<div class="table-container">
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="vaCreditTableBody">
|
|
||||||
<!-- Data will be loaded here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="vaEmptyCreditState" style="display: none;">
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
<h3>No Credit Found</h3>
|
|
||||||
<p>You haven't received any credit yet.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,76 +1,371 @@
|
||||||
<!-- Stats Cards -->
|
<!DOCTYPE html>
|
||||||
<div class="stats-grid">
|
<html lang="en" data-theme="light">
|
||||||
<div class="stat-card">
|
<head>
|
||||||
<div class="stat-content">
|
<meta charset="UTF-8">
|
||||||
<div class="stat-info">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<h3>Active VA</h3>
|
<title>DocVA Client | Dashboard</title>
|
||||||
<div class="stat-value" id="activeVaCount">0</div>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
||||||
|
</head>
|
||||||
|
<body class="dashboard-body">
|
||||||
|
<div id="root">
|
||||||
|
<nav class="dashboard-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-logo">
|
||||||
|
<div class="logo-container">
|
||||||
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-nav">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/dashboard" class="nav-button active">
|
||||||
|
<i class="fas fa-chart-bar"></i>Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/virtual-assistants" class="nav-button">
|
||||||
|
<i class="fas fa-users"></i>Virtual Assistants
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/projects" class="nav-button">
|
||||||
|
<i class="fas fa-file-invoice"></i>Projects
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a href="/client/timesheets" class="nav-button">
|
||||||
|
<i class="fas fa-clock"></i>Timesheets
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/referrals" class="nav-button">
|
||||||
|
<i class="fas fa-share-alt"></i>Referrals
|
||||||
|
</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="user-avatar">TT</div>
|
||||||
|
|
||||||
|
<button class="mobile-menu-button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon blue">
|
</nav>
|
||||||
<i class="fas fa-users fa-xl"></i>
|
<div class="dashboard-container">
|
||||||
</div>
|
<aside class="dashboard-sidebar">
|
||||||
</div>
|
<!-- <div class="sidebar-header">
|
||||||
</div>
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
||||||
<div class="stat-card">
|
</div> -->
|
||||||
<div class="stat-content">
|
<nav class="sidebar-nav">
|
||||||
<div class="stat-info">
|
<ul class="sidebar-list">
|
||||||
<h3>Total VA's</h3>
|
<li class="sidebar-item">
|
||||||
<div class="stat-value" id="totalVaCount">0</div>
|
<a href="/client/dashboard" class="sidebar-link active">
|
||||||
</div>
|
<i class="fas fa-chart-bar sidebar-icon"></i>
|
||||||
<div class="stat-icon blue">
|
<span>Dashboard</span>
|
||||||
<i class="fas fa-users fa-xl"></i>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
<li class="sidebar-item">
|
||||||
</div>
|
<a href="/client/virtual-assistants" class="sidebar-link">
|
||||||
|
<i class="fas fa-users sidebar-icon"></i>
|
||||||
|
<span>Virtual Assistants</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/projects" class="sidebar-link">
|
||||||
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
||||||
|
<span>Projects</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="sidebar-item">
|
||||||
|
<a href="/client/invoices" class="sidebar-link">
|
||||||
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
||||||
|
<span>Invoices</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/referrals" class="sidebar-link">
|
||||||
|
<i class="fas fa-share-alt sidebar-icon"></i>
|
||||||
|
<span>Referrals</span>
|
||||||
|
</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main class="dashboard-main">
|
||||||
|
<div class="dashboard-content">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>Active VA</h3>
|
||||||
|
<div class="stat-value" id="activeVaCount">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon blue">
|
||||||
|
<i class="fas fa-users fa-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>Total VA's</h3>
|
||||||
|
<div class="stat-value" id="totalVaCount">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon blue">
|
||||||
|
<i class="fas fa-users fa-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<h3>Total Projects</h3>
|
<h3>Total Projects</h3>
|
||||||
<div class="stat-value" id="totalProjects">0</div>
|
<div class="stat-value" id="totalProjects">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon green">
|
<div class="stat-icon green">
|
||||||
<i class="fas fa-clock fa-xl"></i>
|
<i class="fas fa-clock fa-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Virtual Assistants List -->
|
|
||||||
<div class="content-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
|
|
||||||
</div>
|
|
||||||
<div class="table-container">
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Virtual Assistant</th>
|
|
||||||
<th>Active Project</th>
|
|
||||||
<th>Total Projects</th>
|
|
||||||
<th>Today Logged Hours</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="vaTableBody">
|
|
||||||
<!-- Data will be loaded here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="vaEmptyState" style="display: none;">
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
<h3>No Virtual Assistants</h3>
|
|
||||||
<p>You haven't assigned any virtual assistants yet.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- Virtual Assistants List -->
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Virtual Assistant</th>
|
||||||
|
<th>Projects Assigned</th>
|
||||||
|
<th>Active Projects</th>
|
||||||
|
<th>Worked Time</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="vaTableBody">
|
||||||
|
<!-- Data will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="vaEmptyState" style="display: none;">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<h3>No Virtual Assistants</h3>
|
||||||
|
<p>You haven't assigned any virtual assistants yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const tnyk = "<%= typeof sf_token !== 'undefined' ? sf_token : '' %>";
|
// Theme management
|
||||||
if (tnyk) {
|
function setTheme(theme) {
|
||||||
localStorage.setItem('knoptpq', tnyk);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
}
|
localStorage.setItem('theme', theme);
|
||||||
</script>
|
}
|
||||||
|
|
||||||
|
function getSystemTheme() {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') || getSystemTheme();
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
// API base URL
|
||||||
|
const API_BASE = '/api/v1/client';
|
||||||
|
|
||||||
|
// Auth token management
|
||||||
|
function getAuthToken() {
|
||||||
|
return localStorage.getItem('knoptpq');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, options = {}) {
|
||||||
|
const token = getAuthToken();
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'token': `${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
localStorage.removeItem('knoptpq');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format time function
|
||||||
|
function formatTime(seconds) {
|
||||||
|
if (!seconds) return 'Not working';
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m`;
|
||||||
|
} else {
|
||||||
|
return 'Just started';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VA card HTML
|
||||||
|
function createVATableRow(va) {
|
||||||
|
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
|
||||||
|
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
|
||||||
|
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
|
||||||
|
const workedTime = formatTime(va.currentSessionDuration);
|
||||||
|
const projectsAssigned = va.projectsAssigned || 0;
|
||||||
|
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
View
|
||||||
|
</a>` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<div class="status-dot ${statusClass}"></div>
|
||||||
|
<span class="status-text ${statusClass}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="va-info">
|
||||||
|
<div class="va-name">${va.name}</div>
|
||||||
|
<div class="va-email">${va.email || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="projects-count">${activeProject}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="projects-count">${projectsAssigned} Projects</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="worked-time">${workedTime}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${viewLink}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load dashboard data
|
||||||
|
async function loadDashboardData() {
|
||||||
|
try {
|
||||||
|
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
||||||
|
if (!response) return;
|
||||||
|
const result = await response.json();
|
||||||
|
// console.log(data)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
const summary = result.summary;
|
||||||
|
|
||||||
|
// Update stats cards
|
||||||
|
document.getElementById('activeVaCount').textContent = summary.activeVAs || 0;
|
||||||
|
document.getElementById('totalVaCount').textContent = summary.totalVAs || 0;
|
||||||
|
document.getElementById('totalProjects').textContent = summary.totalProjects || 0;
|
||||||
|
|
||||||
|
// Update VA list count
|
||||||
|
document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`;
|
||||||
|
|
||||||
|
// Update VA table
|
||||||
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
|
const allVAs = data.allVAs?.total || [];
|
||||||
|
|
||||||
|
if (allVAs.length > 0) {
|
||||||
|
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
|
||||||
|
vaTableBody.style.display = '';
|
||||||
|
vaEmptyState.style.display = 'none';
|
||||||
|
|
||||||
|
// Add event listeners to view links
|
||||||
|
document.querySelectorAll('.view-link').forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const vaId = this.getAttribute('data-va-id');
|
||||||
|
// You can update this link as needed
|
||||||
|
window.location.href = `/client/virtual-assistants/${vaId}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
vaTableBody.innerHTML = '';
|
||||||
|
vaTableBody.style.display = 'none';
|
||||||
|
vaEmptyState.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('Failed to load dashboard data:', result.message);
|
||||||
|
showError('Failed to load dashboard data');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading dashboard data:', error);
|
||||||
|
showError('Error loading dashboard data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
function showError(message) {
|
||||||
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
|
|
||||||
|
vaTableBody.style.display = 'none';
|
||||||
|
vaEmptyState.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<h3>Error</h3>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
vaEmptyState.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', loadDashboardData);
|
||||||
|
|
||||||
|
// Optional: Auto-refresh data every 30 seconds
|
||||||
|
setInterval(loadDashboardData, 30000);
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
var menuToggle = document.querySelector('.mobile-menu-button');
|
||||||
|
var sidebar = document.querySelector('.dashboard-sidebar');
|
||||||
|
menuToggle.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
<!-- Stats Cards -->
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-info">
|
|
||||||
<h3>Unbilled Tips</h3>
|
|
||||||
<div class="stat-value" id="vaUnbilledTips">$0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-icon blue">
|
|
||||||
<i class="fas fa-file-invoice fa-xl"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-info">
|
|
||||||
<h3>Total Tips</h3>
|
|
||||||
<div class="stat-value" id="vaTotalTips">$0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-icon blue">
|
|
||||||
<i class="fas fa-dollar fa-xl"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Virtual Assistants List -->
|
|
||||||
<div class="content-section">
|
|
||||||
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
|
||||||
<a href="/client/virtual-assistants" class="btn btn-secondary">Back</a>
|
|
||||||
<span class="btn btn-primary" id="tipVaAddBtn">Tip Virtual Assistant</span>
|
|
||||||
</div>
|
|
||||||
<div class="table-container">
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Void</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Virtual Assistant Name</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Date Added</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="vaTipTableBody">
|
|
||||||
<!-- Data will be loaded here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="vaEmptyTipState" style="display: none;">
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
<h3>No Tip Found</h3>
|
|
||||||
<p>You haven't gave any Tip yet.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<div class="profile-container">
|
|
||||||
<div class="profile-header">
|
|
||||||
<h1 class="profile-title">Client Profile</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="profile-sections">
|
|
||||||
<!-- Personal Information -->
|
|
||||||
<div class="profile-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<i class="fas fa-user section-icon"></i>
|
|
||||||
<h2 class="section-title">Personal Information</h2>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Full Name</label>
|
|
||||||
<input type="text" class="form-input" id="name" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Email</label>
|
|
||||||
<input type="email" class="form-input" id="email" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Position</label>
|
|
||||||
<input type="text" class="form-input" id="position" readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Address Information -->
|
|
||||||
<div class="profile-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<i class="fas fa-home section-icon"></i>
|
|
||||||
<h2 class="section-title">Address Information</h2>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Address</label>
|
|
||||||
<input type="text" class="form-input" id="address" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">City</label>
|
|
||||||
<input type="text" class="form-input" id="city" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">State</label>
|
|
||||||
<input type="text" class="form-input" id="state" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">ZIP Code</label>
|
|
||||||
<input type="text" class="form-input" id="zip" readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Password Change Section -->
|
|
||||||
<div class="profile-section">
|
|
||||||
<div class="password-section">
|
|
||||||
<button class="change-password-btn" id="updateBankDetailsBtn">
|
|
||||||
<i class="fas fa-credit-card"></i>
|
|
||||||
Update Bank Details
|
|
||||||
</button>
|
|
||||||
<button class="change-password-btn" id="changePasswordBtn">
|
|
||||||
<i class="fas fa-lock"></i>
|
|
||||||
Change Password
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,31 +1,378 @@
|
||||||
<!-- Projects List -->
|
<!DOCTYPE html>
|
||||||
<div class="content-section">
|
<html lang="en" data-theme="light">
|
||||||
<div class="section-header">
|
<head>
|
||||||
<h2 class="section-title">Projects <span class="total-count" id="projectsCount">0
|
<meta charset="UTF-8">
|
||||||
Projects</span></h2>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
</div>
|
<title>DocVA Client | Dashboard</title>
|
||||||
<div class="table-container">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
<div class="table-wrapper">
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
||||||
<table class="data-table">
|
</head>
|
||||||
<thead>
|
<body class="dashboard-body">
|
||||||
<tr>
|
<div id="root">
|
||||||
<th>Name</th>
|
<nav class="dashboard-header">
|
||||||
<th>Hourly Rate</th>
|
<div class="header-content">
|
||||||
<th>Total VA Assigned</th>
|
<div class="header-logo">
|
||||||
<th>Dedicated Hours</th>
|
<div class="logo-container">
|
||||||
</tr>
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
||||||
</thead>
|
</div>
|
||||||
<tbody id="projectsTableBody">
|
</div>
|
||||||
<!-- Data will be loaded here -->
|
|
||||||
</tbody>
|
<div class="header-nav">
|
||||||
</table>
|
<ul class="nav-list">
|
||||||
</div>
|
<li class="nav-item">
|
||||||
<div id="projectsEmptyState" style="display: none;">
|
<a href="/client/dashboard" class="nav-button">
|
||||||
<div class="empty-state">
|
<i class="fas fa-chart-bar"></i>Dashboard
|
||||||
<i class="fas fa-briefcase"></i>
|
</a>
|
||||||
<h3>No Projects</h3>
|
</li>
|
||||||
<p>You don't have any projects yet.</p>
|
<li class="nav-item">
|
||||||
|
<a href="/client/virtual-assistants" class="nav-button">
|
||||||
|
<i class="fas fa-users"></i>Virtual Assistants
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/projects" class="nav-button active">
|
||||||
|
<i class="fas fa-file-invoice"></i>Projects
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a href="/client/timesheets" class="nav-button">
|
||||||
|
<i class="fas fa-clock"></i>Timesheets
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/referrals" class="nav-button">
|
||||||
|
<i class="fas fa-share-alt"></i>Referrals
|
||||||
|
</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="user-avatar">TT</div>
|
||||||
|
|
||||||
|
<button class="mobile-menu-button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<aside class="dashboard-sidebar">
|
||||||
|
<!-- <div class="sidebar-header">
|
||||||
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
||||||
|
</div> -->
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<ul class="sidebar-list">
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/dashboard" class="sidebar-link">
|
||||||
|
<i class="fas fa-chart-bar sidebar-icon"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/virtual-assistants" class="sidebar-link">
|
||||||
|
<i class="fas fa-users sidebar-icon"></i>
|
||||||
|
<span>Virtual Assistants</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/projects" class="sidebar-link active">
|
||||||
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
||||||
|
<span>Projects</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="sidebar-item">
|
||||||
|
<a href="/client/invoices" class="sidebar-link">
|
||||||
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
||||||
|
<span>Invoices</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/referrals" class="sidebar-link">
|
||||||
|
<i class="fas fa-share-alt sidebar-icon"></i>
|
||||||
|
<span>Referrals</span>
|
||||||
|
</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main class="dashboard-main">
|
||||||
|
<div class="dashboard-content">
|
||||||
|
<!-- Projects List -->
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Projects <span class="total-count" id="projectsCount">0 Projects</span></h2>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Hourly Rate</th>
|
||||||
|
<th>Total VA Assigned</th>
|
||||||
|
<th>Dedicated Hours</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="projectsTableBody">
|
||||||
|
<!-- Data will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="projectsEmptyState" style="display: none;">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
<h3>No Projects</h3>
|
||||||
|
<p>You don't have any projects yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br><br>
|
||||||
|
<!-- Virtual Assistants List -->
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vasCount">0 VAs</span></h2>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="vaTableBody">
|
||||||
|
<!-- Data will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="vaEmptyState" style="display: none;">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<h3>No Virtual Assistants</h3>
|
||||||
|
<p>You don't have any virtual assistants assigned yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<script>
|
||||||
|
// Theme management
|
||||||
|
function setTheme(theme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemTheme() {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') || getSystemTheme();
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
// API base URL
|
||||||
|
const API_BASE = '/api/v1/client';
|
||||||
|
|
||||||
|
// Auth token management
|
||||||
|
function getAuthToken() {
|
||||||
|
return localStorage.getItem('knoptpq');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, options = {}) {
|
||||||
|
const token = getAuthToken();
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'token': `${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
localStorage.removeItem('knoptpq');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Project table row HTML
|
||||||
|
function createProjectTableRow(project) {
|
||||||
|
const statusClass = project.status === 'active' ? 'active' : 'inactive';
|
||||||
|
const statusText = project.status === 'active' ? 'Active' : 'Inactive';
|
||||||
|
const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : '-';
|
||||||
|
const dedicatedHours = project.dedicatedHours ? `${project.dedicatedHours}` : '-';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="project-info">
|
||||||
|
<div class="project-name">${project.name || 'Unnamed Project'}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="hourly-rate">${hourlyRate}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="va-count">${project.vaAssigned || 0}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="dedicated-hours">${dedicatedHours}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<div class="status-dot ${statusClass}"></div>
|
||||||
|
<span class="status-text ${statusClass}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VA table row HTML
|
||||||
|
function createVATableRow(va) {
|
||||||
|
const statusClass = va.status === 'active' ? 'active' : 'inactive';
|
||||||
|
const statusText = va.status === 'active' ? 'Active' : 'Inactive';
|
||||||
|
const fullName = `${va.firstName || ''} ${va.lastName || ''}`.trim() || 'Unknown';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="va-info">
|
||||||
|
<div class="va-name">${fullName}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="va-email">${va.email || 'N/A'}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<div class="status-dot ${statusClass}"></div>
|
||||||
|
<span class="status-text ${statusClass}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load dashboard data
|
||||||
|
async function loadDashboardData() {
|
||||||
|
try {
|
||||||
|
const response = await fetchWithAuth(`${API_BASE}/dashboard-projects`);
|
||||||
|
if (!response) return;
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
const counts = result.counts;
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
document.getElementById('projectsCount').textContent = `${counts.totalProjects || 0} Projects`;
|
||||||
|
document.getElementById('vasCount').textContent = `${counts.totalVAs || 0} VAs`;
|
||||||
|
|
||||||
|
// Update Projects table
|
||||||
|
const projectsTableBody = document.getElementById('projectsTableBody');
|
||||||
|
const projectsEmptyState = document.getElementById('projectsEmptyState');
|
||||||
|
const projects = data.projects || [];
|
||||||
|
|
||||||
|
if (projects.length > 0) {
|
||||||
|
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
|
||||||
|
projectsTableBody.style.display = '';
|
||||||
|
projectsEmptyState.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
projectsTableBody.innerHTML = '';
|
||||||
|
projectsTableBody.style.display = 'none';
|
||||||
|
projectsEmptyState.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update VA table
|
||||||
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
|
const vas = data.vas || [];
|
||||||
|
|
||||||
|
if (vas.length > 0) {
|
||||||
|
vaTableBody.innerHTML = vas.map(va => createVATableRow(va)).join('');
|
||||||
|
vaTableBody.style.display = '';
|
||||||
|
vaEmptyState.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
vaTableBody.innerHTML = '';
|
||||||
|
vaTableBody.style.display = 'none';
|
||||||
|
vaEmptyState.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('Failed to load dashboard data:', result.message);
|
||||||
|
showError('Failed to load dashboard data');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading dashboard data:', error);
|
||||||
|
showError('Error loading dashboard data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
function showError(message) {
|
||||||
|
const projectsTableBody = document.getElementById('projectsTableBody');
|
||||||
|
const projectsEmptyState = document.getElementById('projectsEmptyState');
|
||||||
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
|
|
||||||
|
// Hide tables
|
||||||
|
projectsTableBody.style.display = 'none';
|
||||||
|
vaTableBody.style.display = 'none';
|
||||||
|
|
||||||
|
// Show error states
|
||||||
|
projectsEmptyState.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<h3>Error</h3>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
projectsEmptyState.style.display = 'block';
|
||||||
|
|
||||||
|
vaEmptyState.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<h3>Error</h3>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
vaEmptyState.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', loadDashboardData);
|
||||||
|
|
||||||
|
// Optional: Auto-refresh data every 30 seconds
|
||||||
|
setInterval(loadDashboardData, 30000);
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
var menuToggle = document.querySelector('.mobile-menu-button');
|
||||||
|
var sidebar = document.querySelector('.dashboard-sidebar');
|
||||||
|
menuToggle.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
<!-- Virtual Assistants List -->
|
|
||||||
<div class="content-section">
|
|
||||||
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
|
||||||
<a href="/client/virtual-assistants" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-arrow-left"></i> Back to VAs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div id="loadingState" class="loading-spinner">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>Loading VA logs...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div id="emptyState" class="empty-state" style="display: none;">
|
|
||||||
<i class="fas fa-clipboard-list"></i>
|
|
||||||
<h3>No Logs Found</h3>
|
|
||||||
<p>This virtual assistant hasn't logged any time in the last 15 days.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Projects Accordion Container -->
|
|
||||||
<div id="projectsContainer" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,69 +1,371 @@
|
||||||
<!-- Stats Cards -->
|
<!DOCTYPE html>
|
||||||
<div class="stats-grid">
|
<html lang="en" data-theme="light">
|
||||||
<div class="stat-card">
|
<head>
|
||||||
<div class="stat-content">
|
<meta charset="UTF-8">
|
||||||
<div class="stat-info">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<h3>Active VA</h3>
|
<title>DocVA Client | Dashboard</title>
|
||||||
<div class="stat-value" id="activeVaCount">0</div>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
</div>
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
||||||
<div class="stat-icon blue">
|
</head>
|
||||||
<i class="fas fa-users fa-xl"></i>
|
<body class="dashboard-body">
|
||||||
|
<div id="root">
|
||||||
|
<nav class="dashboard-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-logo">
|
||||||
|
<div class="logo-container">
|
||||||
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-nav">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/dashboard" class="nav-button">
|
||||||
|
<i class="fas fa-chart-bar"></i>Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/virtual-assistants" class="nav-button active">
|
||||||
|
<i class="fas fa-users"></i>Virtual Assistants
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/projects" class="nav-button">
|
||||||
|
<i class="fas fa-file-invoice"></i>Projects
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a href="/client/timesheets" class="nav-button">
|
||||||
|
<i class="fas fa-clock"></i>Timesheets
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/client/referrals" class="nav-button">
|
||||||
|
<i class="fas fa-share-alt"></i>Referrals
|
||||||
|
</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="user-avatar">TT</div>
|
||||||
|
|
||||||
|
<button class="mobile-menu-button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<aside class="dashboard-sidebar">
|
||||||
|
<!-- <div class="sidebar-header">
|
||||||
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
||||||
|
</div> -->
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<ul class="sidebar-list">
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/dashboard" class="sidebar-link">
|
||||||
|
<i class="fas fa-chart-bar sidebar-icon"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/virtual-assistants" class="sidebar-link active">
|
||||||
|
<i class="fas fa-users sidebar-icon"></i>
|
||||||
|
<span>Virtual Assistants</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/projects" class="sidebar-link">
|
||||||
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
||||||
|
<span>Projects</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="sidebar-item">
|
||||||
|
<a href="/client/invoices" class="sidebar-link">
|
||||||
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
||||||
|
<span>Invoices</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<a href="/client/referrals" class="sidebar-link">
|
||||||
|
<i class="fas fa-share-alt sidebar-icon"></i>
|
||||||
|
<span>Referrals</span>
|
||||||
|
</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main class="dashboard-main">
|
||||||
|
<div class="dashboard-content">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>Active VA</h3>
|
||||||
|
<div class="stat-value" id="activeVaCount">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon blue">
|
||||||
|
<i class="fas fa-users fa-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>Total VA's</h3>
|
||||||
|
<div class="stat-value" id="totalVaCount">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon blue">
|
||||||
|
<i class="fas fa-users fa-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>Total Projects</h3>
|
||||||
|
<div class="stat-value" id="totalProjects">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon green">
|
||||||
|
<i class="fas fa-clock fa-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Virtual Assistants List -->
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Virtual Assistant</th>
|
||||||
|
<th>Projects Assigned</th>
|
||||||
|
<th>Active Projects</th>
|
||||||
|
<th>Worked Time</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="vaTableBody">
|
||||||
|
<!-- Data will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="vaEmptyState" style="display: none;">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<h3>No Virtual Assistants</h3>
|
||||||
|
<p>You haven't assigned any virtual assistants yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<script>
|
||||||
<div class="stat-content">
|
// Theme management
|
||||||
<div class="stat-info">
|
function setTheme(theme) {
|
||||||
<h3>Total VA's</h3>
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
<div class="stat-value" id="totalVaCount">0</div>
|
localStorage.setItem('theme', theme);
|
||||||
</div>
|
}
|
||||||
<div class="stat-icon blue">
|
|
||||||
<i class="fas fa-users fa-xl"></i>
|
function getSystemTheme() {
|
||||||
</div>
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
// Initialize theme
|
||||||
<div class="stat-content">
|
const savedTheme = localStorage.getItem('theme') || getSystemTheme();
|
||||||
<div class="stat-info">
|
setTheme(savedTheme);
|
||||||
<h3>Total Projects</h3>
|
|
||||||
<div class="stat-value" id="totalProjects">0</div>
|
// API base URL
|
||||||
</div>
|
const API_BASE = '/api/v1/client';
|
||||||
<div class="stat-icon green">
|
|
||||||
<i class="fas fa-clock fa-xl"></i>
|
// Auth token management
|
||||||
</div>
|
function getAuthToken() {
|
||||||
</div>
|
return localStorage.getItem('knoptpq');
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
<!-- Virtual Assistants List -->
|
async function fetchWithAuth(url, options = {}) {
|
||||||
<div class="content-section">
|
const token = getAuthToken();
|
||||||
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
if (!token) {
|
||||||
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
|
window.location.href = '/client/login';
|
||||||
<a href="/client/virtual-assistants/manage-tips" class="btn btn-primary">Manage Tips</a>
|
return;
|
||||||
</div>
|
}
|
||||||
<div class="table-container">
|
|
||||||
<div class="table-wrapper">
|
const response = await fetch(url, {
|
||||||
<table class="data-table">
|
...options,
|
||||||
<thead>
|
headers: {
|
||||||
<tr>
|
'token': `${token}`,
|
||||||
<th>Status</th>
|
'Content-Type': 'application/json',
|
||||||
<th>Virtual Assistant</th>
|
...options.headers,
|
||||||
<th>Active Project</th>
|
}
|
||||||
<th>Total Projects</th>
|
});
|
||||||
<th>Today Logged Hours</th>
|
|
||||||
<th>Action</th>
|
if (response.status === 401) {
|
||||||
</tr>
|
localStorage.removeItem('knoptpq');
|
||||||
</thead>
|
localStorage.removeItem('refreshToken');
|
||||||
<tbody id="vaTableBody">
|
window.location.href = '/client/login';
|
||||||
<!-- Data will be loaded here -->
|
return;
|
||||||
</tbody>
|
}
|
||||||
</table>
|
|
||||||
</div>
|
return response;
|
||||||
<div id="vaEmptyState" style="display: none;">
|
}
|
||||||
|
|
||||||
|
// Format time function
|
||||||
|
function formatTime(seconds) {
|
||||||
|
if (!seconds) return 'Not working';
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m`;
|
||||||
|
} else {
|
||||||
|
return 'Just started';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VA card HTML
|
||||||
|
function createVATableRow(va) {
|
||||||
|
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
|
||||||
|
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
|
||||||
|
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
|
||||||
|
const workedTime = formatTime(va.currentSessionDuration);
|
||||||
|
const projectsAssigned = va.projectsAssigned || 0;
|
||||||
|
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
View
|
||||||
|
</a>` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<div class="status-dot ${statusClass}"></div>
|
||||||
|
<span class="status-text ${statusClass}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="va-info">
|
||||||
|
<div class="va-name">${va.name}</div>
|
||||||
|
<div class="va-email">${va.email || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="projects-count">${activeProject}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="projects-count">${projectsAssigned} Projects</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="worked-time">${workedTime}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${viewLink}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load dashboard data
|
||||||
|
async function loadDashboardData() {
|
||||||
|
try {
|
||||||
|
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
||||||
|
if (!response) return;
|
||||||
|
const result = await response.json();
|
||||||
|
// console.log(data)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
const summary = result.summary;
|
||||||
|
|
||||||
|
// Update stats cards
|
||||||
|
document.getElementById('activeVaCount').textContent = summary.activeVAs || 0;
|
||||||
|
document.getElementById('totalVaCount').textContent = summary.totalVAs || 0;
|
||||||
|
document.getElementById('totalProjects').textContent = summary.totalProjects || 0;
|
||||||
|
|
||||||
|
// Update VA list count
|
||||||
|
document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`;
|
||||||
|
|
||||||
|
// Update VA table
|
||||||
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
|
const allVAs = data.allVAs?.total || [];
|
||||||
|
|
||||||
|
if (allVAs.length > 0) {
|
||||||
|
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
|
||||||
|
vaTableBody.style.display = '';
|
||||||
|
vaEmptyState.style.display = 'none';
|
||||||
|
|
||||||
|
// Add event listeners to view links
|
||||||
|
document.querySelectorAll('.view-link').forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const vaId = this.getAttribute('data-va-id');
|
||||||
|
// You can update this link as needed
|
||||||
|
window.location.href = `/client/virtual-assistants/${vaId}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
vaTableBody.innerHTML = '';
|
||||||
|
vaTableBody.style.display = 'none';
|
||||||
|
vaEmptyState.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('Failed to load dashboard data:', result.message);
|
||||||
|
showError('Failed to load dashboard data');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading dashboard data:', error);
|
||||||
|
showError('Error loading dashboard data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
function showError(message) {
|
||||||
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
|
|
||||||
|
vaTableBody.style.display = 'none';
|
||||||
|
vaEmptyState.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
<h3>No Virtual Assistants</h3>
|
<h3>Error</h3>
|
||||||
<p>You haven't assigned any virtual assistants yet.</p>
|
<p>${message}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`;
|
||||||
</div>
|
vaEmptyState.style.display = 'block';
|
||||||
</div>
|
}
|
||||||
|
|
||||||
|
// Load data when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', loadDashboardData);
|
||||||
|
|
||||||
|
// Optional: Auto-refresh data every 30 seconds
|
||||||
|
setInterval(loadDashboardData, 30000);
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
var menuToggle = document.querySelector('.mobile-menu-button');
|
||||||
|
var sidebar = document.querySelector('.dashboard-sidebar');
|
||||||
|
menuToggle.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -213,7 +213,7 @@
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img id="logo-img" src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
|
<img src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
|
|
@ -224,7 +224,7 @@
|
||||||
<input type="password" name="password" placeholder="Enter your password *" required />
|
<input type="password" name="password" placeholder="Enter your password *" required />
|
||||||
<a href="/client/forgot-password" class="forgot">Forgot password?</a>
|
<a href="/client/forgot-password" class="forgot">Forgot password?</a>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Log in using password</button>
|
<button type="submit" class="btn btn-primary">🔒 Log in using password</button>
|
||||||
|
|
||||||
<!-- <div class="divider">OR</div>
|
<!-- <div class="divider">OR</div>
|
||||||
|
|
||||||
|
|
@ -236,10 +236,6 @@
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
document.getElementById("logo-img").src =
|
|
||||||
theme === "dark"
|
|
||||||
? "/public/images/logo-white-vertical.svg"
|
|
||||||
: "/public/images/logo-colored-vertical.svg";
|
|
||||||
root.setAttribute("data-theme", theme);
|
root.setAttribute("data-theme", theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,14 +276,14 @@
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(data.message || "Login failed");
|
throw new Error(data.message || "Login failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if(data.userType === "CLIENT") {
|
// 🟢 Save token
|
||||||
localStorage.setItem("knoptpq", data.accessToken);
|
localStorage.setItem("knoptpq", data.accessToken);
|
||||||
window.location.href = "/client/dashboard";
|
// localStorage.setItem("refreshToken", data.refreshToken);
|
||||||
} else {
|
|
||||||
window.location.href = "/download-tracker";
|
// 🟢 Redirect
|
||||||
}
|
window.location.href = "/client/dashboard";
|
||||||
} else {
|
} else {
|
||||||
alert("No token received. Please try again.");
|
alert("No token received. Please try again.");
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
<script>
|
<script>
|
||||||
const osButtons = document.querySelectorAll(".os-btn");
|
const osButtons = document.querySelectorAll(".os-btn");
|
||||||
const downloadBtn = document.getElementById("downloadBtn");
|
const downloadBtn = document.getElementById("downloadBtn");
|
||||||
const version = "<%= version || '1.1.1' %>";
|
const version = "<%= version || '3.2.1' %>";
|
||||||
|
|
||||||
let selectedOS = "windows";
|
let selectedOS = "windows";
|
||||||
|
|
||||||
|
|
@ -171,21 +171,7 @@
|
||||||
|
|
||||||
// -------- Download --------
|
// -------- Download --------
|
||||||
downloadBtn.addEventListener("click", () => {
|
downloadBtn.addEventListener("click", () => {
|
||||||
const activeBtn = document.querySelector(".os-btn.active")
|
window.location.href = `/download-file?os=${selectedOS}`;
|
||||||
const os = activeBtn.dataset.os;
|
|
||||||
|
|
||||||
const link = document.createElement("a");
|
|
||||||
|
|
||||||
if(os === "mac") {
|
|
||||||
link.href = `/public/builds/build-1.1.1.1/mac/VA Tracker-1.1.1-universal.dmg`;
|
|
||||||
link.setAttribute("download", "");
|
|
||||||
} else if (os === "windows") {
|
|
||||||
link.href = `/public/builds/build-1.1.1.1/windows/VA Tracker-Setup-1.1.1.exe`;
|
|
||||||
link.setAttribute("download", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
link.click()
|
|
||||||
// window.location.href = ``;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,123 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>DocVA | Reset Password</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--bg: #ffffff;
|
||||||
|
--text: #111827;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--btn-bg: #0a0a8f;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--bg: #121212;
|
||||||
|
--text: #f9fafb;
|
||||||
|
--card-bg: #1e1e1e;
|
||||||
|
--border: #334155;
|
||||||
|
--btn-bg: #3b82f6;
|
||||||
|
--btn-text: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 170px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: background 0.3s, border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: border 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] input {
|
||||||
|
background: #0f172a;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--btn-bg);
|
||||||
|
color: var(--btn-text);
|
||||||
|
border: none;
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="logo">
|
<h2>DocTracker</h2>
|
||||||
<img id="logo-img" src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
|
|
||||||
</div>
|
|
||||||
<form class="card">
|
<form class="card">
|
||||||
<h2>Reset your password</h2>
|
<h2>Reset your password</h2>
|
||||||
<input type="email" placeholder="Enter your email address*" required />
|
<input type="email" placeholder="Enter your email address*" required />
|
||||||
|
|
@ -12,24 +127,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
function setTheme(theme) {
|
|
||||||
document.getElementById("logo-img").src =
|
|
||||||
theme === "dark"
|
|
||||||
? "/public/images/logo-white-vertical.svg"
|
|
||||||
: "/public/images/logo-colored-vertical.svg";
|
|
||||||
root.setAttribute("data-theme", theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemTheme() {
|
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
||||||
? "dark"
|
|
||||||
: "light";
|
|
||||||
}
|
|
||||||
|
|
||||||
setTheme(getSystemTheme());
|
|
||||||
|
|
||||||
function validateEmail(email) {
|
function validateEmail(email) {
|
||||||
const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||||
return emailRegex.test(email);
|
return emailRegex.test(email);
|
||||||
|
|
@ -103,3 +200,6 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" data-theme="light">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>
|
|
||||||
<%= title || 'Auth' %>
|
|
||||||
</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<%- body %>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>
|
|
||||||
<%= title || 'Dashboard' %>
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/public/css/dashboard.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
||||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="dashboard-body">
|
|
||||||
|
|
||||||
<%- include('../partials/header') %>
|
|
||||||
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<%- include('../partials/sidebar') %>
|
|
||||||
|
|
||||||
<main class="dashboard-main">
|
|
||||||
<div class="dashboard-content">
|
|
||||||
<%- body %>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%- include('../partials/va-modal') %>
|
|
||||||
<%- include('../partials/logout-modal') %>
|
|
||||||
<%- include('../partials/tip-modal') %>
|
|
||||||
<%- include('../partials/projects-modal') %>
|
|
||||||
|
|
||||||
<%- include('../partials/password-modal') %>
|
|
||||||
|
|
||||||
<%- include('../partials/bankDetails-modal') %>
|
|
||||||
|
|
||||||
<!-- COMMON SIDEBAR + LOGOUT -->
|
|
||||||
<script src="/public/js/common/sidebar.js"></script>
|
|
||||||
<script src="/public/js/common/header.js"></script>
|
|
||||||
|
|
||||||
<!-- COMMON -->
|
|
||||||
<script src="/public/js/common/theme.js"></script>
|
|
||||||
<script src="/public/js/common/auth.js"></script>
|
|
||||||
<script src="/public/js/common/logout.js"></script>
|
|
||||||
|
|
||||||
<!-- PAGE -->
|
|
||||||
<% if (typeof pageScript !=='undefined' ) { %>
|
|
||||||
<script src="/public/js/pages/<%= pageScript %>"></script>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>404 - Page Not Found</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg: #ffffff;
|
|
||||||
--text: #222222;
|
|
||||||
--subtext: #555555;
|
|
||||||
--button-bg: #6c63ff;
|
|
||||||
--button-text: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode automatic */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg: #0f0f0f;
|
|
||||||
--text: #ffffff;
|
|
||||||
--subtext: #bbbbbb;
|
|
||||||
--button-bg: #6c63ff;
|
|
||||||
--button-text: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100vh;
|
|
||||||
font-family: Inter, system-ui, sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 420px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 7rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-top: 10px;
|
|
||||||
color: var(--subtext);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 14px 26px;
|
|
||||||
background: var(--button-bg);
|
|
||||||
color: var(--button-text);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: inline-block;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 150px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
h1 {
|
|
||||||
font-size: 4.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<%- body %>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
|
||||||
<title>DocVA | Reset Password</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root[data-theme="light"] {
|
|
||||||
--bg: #ffffff;
|
|
||||||
--text: #111827;
|
|
||||||
--card-bg: #ffffff;
|
|
||||||
--border: #e5e7eb;
|
|
||||||
--btn-bg: #0a0a8f;
|
|
||||||
--btn-text: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-theme="dark"] {
|
|
||||||
--bg: #121212;
|
|
||||||
--text: #f9fafb;
|
|
||||||
--card-bg: #1e1e1e;
|
|
||||||
--border: #334155;
|
|
||||||
--btn-bg: #3b82f6;
|
|
||||||
--btn-text: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
transition: background-color 0.3s, color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 380px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 170px;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 2rem;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
|
||||||
transition: background 0.3s, border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.8rem;
|
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: #fff;
|
|
||||||
color: #111;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: border 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-theme="dark"] input {
|
|
||||||
background: #0f172a;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
background: var(--btn-bg);
|
|
||||||
color: var(--btn-text);
|
|
||||||
border: none;
|
|
||||||
padding: 0.8rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text);
|
|
||||||
text-decoration: none;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
opacity: 1;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo img {
|
|
||||||
width: 170px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<%- body %>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -13,7 +13,5 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%- body %>
|
<%- body %>
|
||||||
|
|
||||||
<script src="/public/js/common/theme.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
|
||||||
<title>Client Registration</title>
|
|
||||||
|
|
||||||
<link href="/public/css/va-register.css" rel="stylesheet">
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<%- body %>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Live Screen Feed</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
background: #0f0f0f;
|
|
||||||
color: white;
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SIDEBAR */
|
|
||||||
.sidebar {
|
|
||||||
width: 280px;
|
|
||||||
background: #181818;
|
|
||||||
border-right: 1px solid #2b2b2b;
|
|
||||||
padding: 15px;
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: transform 0.35s ease;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar h3 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
background: #252525;
|
|
||||||
border: 1px solid #333;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app:hover {
|
|
||||||
background: #333;
|
|
||||||
transform: translateX(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app.active {
|
|
||||||
background: #6c63ff;
|
|
||||||
border-color: #6c63ff;
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 0 10px rgba(108, 99, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MAIN CONTENT */
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
padding: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selectedAppTitle {
|
|
||||||
font-size: 20px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#liveImage {
|
|
||||||
width: 90%;
|
|
||||||
max-width: 900px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 2px solid #444;
|
|
||||||
object-fit: contain;
|
|
||||||
background: black;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 0.8;
|
|
||||||
color: #bbb;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MOBILE RESPONSIVE */
|
|
||||||
.mobile-toggle {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 15px;
|
|
||||||
left: 15px;
|
|
||||||
background: #6C63FF;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
z-index: 9999;
|
|
||||||
/* VERY HIGH: always visible */
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-toggle span {
|
|
||||||
display: block;
|
|
||||||
width: 26px;
|
|
||||||
height: 3px;
|
|
||||||
background: #fff;
|
|
||||||
margin: 5px 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar Hidden on Mobile */
|
|
||||||
@media (max-width: 780px) {
|
|
||||||
body {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.open {
|
|
||||||
transform: translateX(0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-toggle {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding-top: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay when sidebar opens */
|
|
||||||
#overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
display: none;
|
|
||||||
z-index: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<%- body %>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
|
||||||
<title>Virtual Assistant Registration</title>
|
|
||||||
|
|
||||||
<link href="/public/css/va-register.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
select option:disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<%- body %>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,10 +1,105 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>404 - Page Not Found</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #ffffff;
|
||||||
|
--text: #222222;
|
||||||
|
--subtext: #555555;
|
||||||
|
--button-bg: #6c63ff;
|
||||||
|
--button-text: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode automatic */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #0f0f0f;
|
||||||
|
--text: #ffffff;
|
||||||
|
--subtext: #bbbbbb;
|
||||||
|
--button-bg: #6c63ff;
|
||||||
|
--button-text: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: Inter, system-ui, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 420px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--subtext);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 14px 26px;
|
||||||
|
background: var(--button-bg);
|
||||||
|
color: var(--button-text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 150px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img src="/public/images/logo-white-vertical.svg" alt="404 Icon">
|
<img src="/public/images/logo-white-vertical.svg" alt="404 Icon">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
<p>Oops! The page you're looking for does not exist.</p>
|
<p>Oops! The page you're looking for does not exist.</p>
|
||||||
<a href="/client/login" class="btn">Go Home</a>
|
<a href="/" class="btn">Go Home</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,3 +124,6 @@
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme);
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
<div id="bankDetailsModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title">Update Bank Details</h2>
|
|
||||||
<button class="modal-close" onclick="closeBankDetailsModal()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="payment-form" class="modal-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=" " oninput="validateCVVVV(this)" onkeypress="return preventInvalidCVVChars(event)" maxlength="4" required>
|
|
||||||
<label for="security-code" class="input-label">Security Code *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="security-code-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Please enter a valid security code
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<input type="text" id="card-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
|
|
||||||
<label for="card-zip" class="input-label">ZIP Code *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="card-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
ZIP code should contain only numbers
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="payment-option" onclick="selectPaymentOption('bank')">
|
|
||||||
<h4>Bank Account (ACH)</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=" " oninput="validateBankName(this)" onkeypress="return preventInvalidBankNameChars(event)" required>
|
|
||||||
<label for="bank-name" class="input-label">Bank Name *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="bank-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Bank name should contain letters and cannot be only numbers or special characters
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<input type="text" id="account-number" class="input-field" placeholder=" " oninput="validateAccountNumber(this)" onkeypress="return preventInvalidAccountNumberChars(event)" required>
|
|
||||||
<label for="account-number" class="input-label">Account Number *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="account-number-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Please enter a valid account number
|
|
||||||
</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="" disable selected style="display: none"></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=" " oninput="validateAccountHolderName(this)" onkeypress="return preventInvalidAccountHolderChars(event)" required>
|
|
||||||
<label for="account-holder-name" class="input-label">Account Holder Name *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="account-holder-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Account holder name should only contain letters and spaces
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" class="cancel-btn" onclick="closeBankDetailsModal()">Cancel</button>
|
|
||||||
<button type="submit" class="save-btn" id="saveBankDetailsBtn">Update Bank Details</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
<nav class="dashboard-header">
|
|
||||||
<div class="header-content">
|
|
||||||
<div class="header-logo">
|
|
||||||
<div class="logo-container">
|
|
||||||
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-nav">
|
|
||||||
<ul class="nav-list">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="/client/dashboard" class="nav-button">
|
|
||||||
<i class="fas fa-chart-bar"></i>Dashboard
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="/client/virtual-assistants" class="nav-button">
|
|
||||||
<i class="fas fa-users"></i>Virtual Assistants
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="/client/projects" class="nav-button">
|
|
||||||
<i class="fas fa-file-invoice"></i>Projects
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<button class="mobile-menu-button">
|
|
||||||
<i class="fas fa-bars"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Profile Dropdown -->
|
|
||||||
<div class="header-profile">
|
|
||||||
<div class="profile-trigger">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="profile-menu">
|
|
||||||
|
|
||||||
<a href="/client/profile">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
Profile
|
|
||||||
</a>
|
|
||||||
<!-- <a href="/client/account-settings">
|
|
||||||
<i class="fas fa-cog"></i>
|
|
||||||
Account Settings
|
|
||||||
</a> -->
|
|
||||||
<!-- <a href="/client/billing">
|
|
||||||
<i class="fas fa-credit-card"></i>
|
|
||||||
Billing & Payments
|
|
||||||
</a> -->
|
|
||||||
|
|
||||||
<div class="menu-divider"></div>
|
|
||||||
|
|
||||||
<button class="logout-btn" onclick="openLogoutModal()">
|
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<div id="logoutModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title">Confirm Logout</h2>
|
|
||||||
<button class="modal-close" onclick="closeLogoutModal()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="logout-modal-body">
|
|
||||||
<div class="logout-icon">
|
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
|
||||||
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
|
|
||||||
<div class="logout-actions">
|
|
||||||
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
|
||||||
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<div id="passwordModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title">Change Password</h2>
|
|
||||||
<button class="modal-close" onclick="closePasswordModal()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form class="modal-form" id="passwordForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Current Password</label>
|
|
||||||
<input type="password" class="form-input" id="currentPassword" required>
|
|
||||||
<div class="error-message" id="currentPasswordError"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">New Password</label>
|
|
||||||
<input type="password" class="form-input" id="newPassword" required>
|
|
||||||
<div class="error-message" id="newPasswordError"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Confirm New Password</label>
|
|
||||||
<input type="password" class="form-input" id="confirmPassword" required>
|
|
||||||
<div class="error-message" id="confirmPasswordError"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" class="cancel-btn" onclick="closePasswordModal()">Cancel</button>
|
|
||||||
<button type="submit" class="save-btn" id="savePasswordBtn">Change Password</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<div id="projectModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
|
|
||||||
<h2 class="modal-title" id="modalProjectName">Project Details</h2>
|
|
||||||
<button class="modal-close" id="modalClose">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="va-details-section">
|
|
||||||
<h3 class="va-section-title">
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
Virtual Assistants
|
|
||||||
<span class="va-count-badge" id="vaCountBadge">0</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="va-table-container">
|
|
||||||
<table class="va-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Virtual Assistant</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Activity</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="modalVATableBody">
|
|
||||||
<!-- VA details will be loaded here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div id="modalNoVAs" class="no-vas-message" style="display: none;">
|
|
||||||
<i class="fas fa-user-friends"></i>
|
|
||||||
<h4>No Virtual Assistants</h4>
|
|
||||||
<p>No VAs are currently assigned to this project.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<aside class="dashboard-sidebar" id="dashboardSidebar">
|
|
||||||
<nav class="sidebar-nav">
|
|
||||||
<ul class="sidebar-list">
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="/client/profile" class="sidebar-link">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
<span>Profile</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="/client/dashboard" class="sidebar-link">
|
|
||||||
<i class="fas fa-chart-bar"></i>
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="/client/virtual-assistants" class="sidebar-link">
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
<span>Virtual Assistants</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="/client/projects" class="sidebar-link">
|
|
||||||
<i class="fas fa-file-invoice"></i>
|
|
||||||
<span>Projects</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="/client/credits" class="sidebar-link">
|
|
||||||
<i class="fas fa-wallet"></i>
|
|
||||||
<span>Credits</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="javascript:void(0)" id="logoutButton" class="sidebar-link logout">
|
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
|
||||||
<span>Logout</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<div id="tipModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title">Tip Virtual Assistant</h2>
|
|
||||||
<button class="modal-close" onclick="closeTipModal()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form class="modal-form" id="tipForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Select Virtual Assistant</label>
|
|
||||||
<select name="vaForTip" class="form-input" id="vaForTip" required>
|
|
||||||
<option value="">-- Select Virtual Assistant --</option>
|
|
||||||
<!-- Options will be populated dynamically -->
|
|
||||||
</select>
|
|
||||||
<div class="error-message" id="vaForTipError"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Tip Amount</label>
|
|
||||||
<input type="text" class="form-input" id="vaTipAmount" required>
|
|
||||||
<div class="error-message" id="vaTipAmountError"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Tip Description</label>
|
|
||||||
<textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
|
|
||||||
<div class="error-message" id="vaTipDescriptionError"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
|
|
||||||
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<div id="vaModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="modalVAName">VA Details</h2>
|
|
||||||
<button class="modal-close" onclick="closeVAModal()">✖</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<table class="va-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Project</th>
|
|
||||||
<th>Rate</th>
|
|
||||||
<th>Time</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="modalProjectsTableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div id="modalNoProjects" class="empty-state" style="display:none">
|
|
||||||
No Projects Assigned
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
<div id="vaModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
|
|
||||||
<h2 class="modal-title" id="modalVAName">VA Details</h2>
|
|
||||||
<button class="modal-close" onclick="closeVAModal()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<!-- Projects Section -->
|
|
||||||
<div class="va-details-section">
|
|
||||||
<h3 class="va-section-title">
|
|
||||||
<i class="fas fa-briefcase"></i>
|
|
||||||
Projects
|
|
||||||
<span class="va-count-badge" id="projectsCountBadge">0</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="va-table-container">
|
|
||||||
<table class="va-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Project Name</th>
|
|
||||||
<th>Hourly Rate</th>
|
|
||||||
<th>Worked Time</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="modalProjectsTableBody">
|
|
||||||
<!-- Project details will be loaded here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div id="modalNoProjects" class="no-vas-message" style="display: none;">
|
|
||||||
<i class="fas fa-briefcase"></i>
|
|
||||||
<h4>No Projects</h4>
|
|
||||||
<p>This VA is not assigned to any projects.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Button -->
|
|
||||||
<div class="modal-actions" id="modalActions" style="display: none;">
|
|
||||||
<a href="#" class="view-session-btn" id="viewSessionBtn" target="_blank">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
View Live Session
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
<div class="container">
|
<!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 -->
|
<!-- Sidebar for desktop -->
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
|
|
@ -9,7 +21,7 @@
|
||||||
<nav class="progress-steps">
|
<nav class="progress-steps">
|
||||||
<div class="step active" data-step="1" onclick="goToStep(1)">
|
<div class="step active" data-step="1" onclick="goToStep(1)">
|
||||||
<div class="step-number">1</div>
|
<div class="step-number">1</div>
|
||||||
<!-- <div class="step-number">✓</div> -->
|
<!-- <div class="step-number">✓</div> -->
|
||||||
<div class="step-text">Create Your Account</div>
|
<div class="step-text">Create Your Account</div>
|
||||||
<div class="step-connector"></div>
|
<div class="step-connector"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,7 +59,7 @@
|
||||||
<ol>
|
<ol>
|
||||||
<li class="step active" data-step="1" onclick="goToStep(1)">
|
<li class="step active" data-step="1" onclick="goToStep(1)">
|
||||||
<div class="step-number">1</div>
|
<div class="step-number">1</div>
|
||||||
<!-- <div class="step-number">✓</div> -->
|
<!-- <div class="step-number">✓</div> -->
|
||||||
<div class="step-text">Create Account</div>
|
<div class="step-text">Create Account</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="step" data-step="2" onclick="goToStep(2)">
|
<li class="step" data-step="2" onclick="goToStep(2)">
|
||||||
|
|
@ -86,47 +98,19 @@
|
||||||
<form id="account-form">
|
<form id="account-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="first-name" class="input-field" placeholder=" " oninput="validateFirstName(this)" required>
|
<input type="text" id="company-name" class="input-field" placeholder=" " oninput="validateCompanyName(this)" required>
|
||||||
<label for="first-name" class="input-label">First Name *</label>
|
<label for="company-name" class="input-label">Company / Client Name *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="first-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
<div class="invalid-feedback" id="company-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
First name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
|
Company name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="last-name" class="input-field" placeholder=" " oninput="validateLastName(this)" required>
|
<input type="email" id="email" class="input-field" placeholder=" " required>
|
||||||
<label for="last-name" class="input-label">Last Name *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="last-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Last name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<input type="email" id="email" class="input-field" placeholder=" " value="<%= email %>"
|
|
||||||
readonly disabled required>
|
|
||||||
<label for="email" class="input-label">Email *</label>
|
<label for="email" class="input-label">Email *</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="new-password">
|
|
||||||
<label for="password" class="input-label">Create Password *</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="new-password">
|
|
||||||
<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 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>
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<div></div> <!-- Empty div for spacing -->
|
<div></div> <!-- Empty div for spacing -->
|
||||||
<button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
|
<button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
|
||||||
|
|
@ -141,15 +125,6 @@
|
||||||
<div class="error-message" id="step-2-error-message"></div>
|
<div class="error-message" id="step-2-error-message"></div>
|
||||||
</div>
|
</div>
|
||||||
<form id="business-form">
|
<form id="business-form">
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<input type="text" id="company-name" class="input-field" placeholder=" " oninput="validateCompanyName(this)" required>
|
|
||||||
<label for="company-name" class="input-label">Company Name *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="company-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Company name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="address" class="input-field" placeholder=" " required>
|
<input type="text" id="address" class="input-field" placeholder=" " required>
|
||||||
|
|
@ -158,21 +133,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="city" class="input-field" placeholder=" "
|
<input type="text" id="city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
|
||||||
oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)"
|
|
||||||
required>
|
|
||||||
<label for="city" class="input-label">City *</label>
|
<label for="city" class="input-label">City *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="city-error"
|
<div class="invalid-feedback" id="city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
City name should contain letters and cannot be only numbers or special characters
|
City name should contain letters and cannot be only numbers or special characters
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<!-- <input type="text" id="state" class="input-field" placeholder=" " required> -->
|
<!-- <input type="text" id="state" class="input-field" placeholder=" " required> -->
|
||||||
<select id="state" class="select-field" required>
|
<select id="state" class="select-field" required>
|
||||||
<option value="" disable selected style="display: none"></option>
|
<option value=""></option>
|
||||||
<!-- States will be populated by JavaScript -->
|
<!-- States will be populated by JavaScript -->
|
||||||
</select>
|
</select>
|
||||||
<label for="state" class="input-label">State *</label>
|
<label for="state" class="input-label">State *</label>
|
||||||
|
|
@ -181,14 +153,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="zip" class="input-field" placeholder=" "
|
<input type="text" id="zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
|
||||||
oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)"
|
|
||||||
maxlength="10" required>
|
|
||||||
<label for="zip" class="input-label">ZIP *</label>
|
<label for="zip" class="input-label">ZIP *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="zip-error"
|
<div class="invalid-feedback" id="zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
ZIP code should contain only numbers
|
||||||
Invalid Zip code
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|
@ -223,21 +192,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="billing-city" class="input-field" placeholder=" "
|
<input type="text" id="billing-city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
|
||||||
oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)"
|
|
||||||
required>
|
|
||||||
<label for="billing-city" class="input-label">City *</label>
|
<label for="billing-city" class="input-label">City *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="billing-city-error"
|
<div class="invalid-feedback" id="billing-city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
City name should contain letters and cannot be only numbers or special characters
|
City name should contain letters and cannot be only numbers or special characters
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<!-- <input type="text" id="billing-state" class="input-field" placeholder=" " required> -->
|
<!-- <input type="text" id="billing-state" class="input-field" placeholder=" " required> -->
|
||||||
<select id="billing-state" class="select-field" required>
|
<select id="billing-state" class="select-field" required>
|
||||||
<option value="" disable selected style="display: none"></option>
|
<option value=""></option>
|
||||||
<!-- States will be populated by JavaScript -->
|
<!-- States will be populated by JavaScript -->
|
||||||
</select>
|
</select>
|
||||||
<label for="billing-state" class="input-label">State *</label>
|
<label for="billing-state" class="input-label">State *</label>
|
||||||
|
|
@ -245,13 +211,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="billing-zip" class="input-field" placeholder=" "
|
<input type="text" id="billing-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
|
||||||
oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)"
|
|
||||||
maxlength="10" required>
|
|
||||||
<label for="billing-zip" class="input-label">ZIP *</label>
|
<label for="billing-zip" class="input-label">ZIP *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="billing-zip-error"
|
<div class="invalid-feedback" id="billing-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
ZIP code should contain only numbers
|
ZIP code should contain only numbers
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -279,31 +242,24 @@
|
||||||
<div class="error-message" id="step-4-error-message"></div>
|
<div class="error-message" id="step-4-error-message"></div>
|
||||||
</div>
|
</div>
|
||||||
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
|
<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 starting the project.
|
We will use the details below in the agreement that we will send to your prior to entering the project.
|
||||||
</p>
|
</p>
|
||||||
<form id="signatory-form">
|
<form id="signatory-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="position" class="input-field" placeholder=" "
|
<input type="text" id="position" class="input-field" placeholder=" " oninput="validatePosition(this)" onkeypress="return preventInvalidPositionChars(event)" required>
|
||||||
oninput="validatePosition(this)"
|
<label for="position" class="input-label">Position (Owner / CEO / Manager / etc.) *</label>
|
||||||
onkeypress="return preventInvalidPositionChars(event)" required>
|
|
||||||
<label for="position" class="input-label">Position (Owner / CEO / Manager / etc.)
|
|
||||||
*</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="position-error"
|
<div class="invalid-feedback" id="position-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Position should only contain letters, spaces, and common punctuation
|
Position should only contain letters, spaces, and common punctuation
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="signatory-name" class="input-field" placeholder=" "
|
<input type="text" id="signatory-name" class="input-field" placeholder=" " oninput="validateSignatoryName(this)" onkeypress="return preventNumbers(event)" required>
|
||||||
oninput="validateSignatoryName(this)" onkeypress="return preventNumbers(event)"
|
|
||||||
required>
|
|
||||||
<label for="signatory-name" class="input-label">Name *</label>
|
<label for="signatory-name" class="input-label">Name *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="signatory-name-error"
|
<div class="invalid-feedback" id="signatory-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Name should not contain numbers
|
Name should not contain numbers
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -341,63 +297,50 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="expiry-date" class="input-field" placeholder=" "
|
<input type="text" id="expiry-date" class="input-field" placeholder=" " required>
|
||||||
required>
|
|
||||||
<label for="expiry-date" class="input-label">Expiry Date (MM/YY) *</label>
|
<label for="expiry-date" class="input-label">Expiry Date (MM/YY) *</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="security-code" class="input-field" placeholder=" "
|
<input type="text" id="security-code" class="input-field" placeholder=" " oninput="validateCVVVV(this)" nkeypress="return preventInvalidCVVChars(event)" maxlength="4" required>
|
||||||
oninput="validateCVVVV(this)"
|
|
||||||
nkeypress="return preventInvalidCVVChars(event)" maxlength="4" required>
|
|
||||||
<label for="security-code" class="input-label">Security Code *</label>
|
<label for="security-code" class="input-label">Security Code *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="security-code-error"
|
<div class="invalid-feedback" id="security-code-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Please enter a valid security code
|
Please enter a valid security code
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="card-zip" class="input-field" placeholder=" "
|
<input type="text" id="card-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
|
||||||
oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)"
|
|
||||||
maxlength="10" required>
|
|
||||||
<label for="card-zip" class="input-label">ZIP Code *</label>
|
<label for="card-zip" class="input-label">ZIP Code *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="card-zip-error"
|
<div class="invalid-feedback" id="card-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
ZIP code should contain only numbers
|
ZIP code should contain only numbers
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="payment-option" onclick="selectPaymentOption('bank')">
|
<div class="payment-option" onclick="selectPaymentOption('bank')">
|
||||||
<h4>Bank Account (ACH)</h4>
|
<h4>Bank Account (ACR)</h4>
|
||||||
<p>Direct bank transfer</p>
|
<p>Direct bank transfer</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="bank-details" class="payment-details">
|
<div id="bank-details" class="payment-details">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="bank-name" class="input-field" placeholder=" "
|
<input type="text" id="bank-name" class="input-field" placeholder=" " oninput="validateBankName(this)" onkeypress="return preventInvalidBankNameChars(event)" required>
|
||||||
oninput="validateBankName(this)"
|
|
||||||
onkeypress="return preventInvalidBankNameChars(event)" required>
|
|
||||||
<label for="bank-name" class="input-label">Bank Name *</label>
|
<label for="bank-name" class="input-label">Bank Name *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="bank-name-error"
|
<div class="invalid-feedback" id="bank-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Bank name should contain letters and cannot be only numbers or special characters
|
Bank name should contain letters and cannot be only numbers or special characters
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="account-number" class="input-field" placeholder=" "
|
<input type="text" id="account-number" class="input-field" placeholder=" " oninput="validateAccountNumber(this)" onkeypress="return preventInvalidAccountNumberChars(event)" required>
|
||||||
oninput="validateAccountNumber(this)"
|
|
||||||
onkeypress="return preventInvalidAccountNumberChars(event)" required>
|
|
||||||
<label for="account-number" class="input-label">Account Number *</label>
|
<label for="account-number" class="input-label">Account Number *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="account-number-error"
|
<div class="invalid-feedback" id="account-number-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Please enter a valid account number
|
Please enter a valid account number
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -410,7 +353,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<select id="account-type" class="select-field" required>
|
<select id="account-type" class="select-field" required>
|
||||||
<option value="" disable selected style="display: none"></option>
|
<option value=""></option>
|
||||||
<option value="checking">Checking</option>
|
<option value="checking">Checking</option>
|
||||||
<option value="savings">Savings</option>
|
<option value="savings">Savings</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -419,13 +362,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="account-holder-name" class="input-field" placeholder=" "
|
<input type="text" id="account-holder-name" class="input-field" placeholder=" " oninput="validateAccountHolderName(this)" onkeypress="return preventInvalidAccountHolderChars(event)" required>
|
||||||
oninput="validateAccountHolderName(this)"
|
|
||||||
onkeypress="return preventInvalidAccountHolderChars(event)" required>
|
|
||||||
<label for="account-holder-name" class="input-label">Account Holder Name *</label>
|
<label for="account-holder-name" class="input-label">Account Holder Name *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="account-holder-name-error"
|
<div class="invalid-feedback" id="account-holder-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Account holder name should only contain letters and spaces
|
Account holder name should only contain letters and spaces
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -442,26 +382,13 @@
|
||||||
<div class="success-message">
|
<div class="success-message">
|
||||||
<div class="success-icon">✓</div>
|
<div class="success-icon">✓</div>
|
||||||
<h3>Registration Complete!</h3>
|
<h3>Registration Complete!</h3>
|
||||||
<p>Your account has been successfully created. You will receive a confirmation email shortly.
|
<p>Your account has been successfully created. You will receive a confirmation email shortly.</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/public/js/va-client-register.js"></script>
|
<script src="/public/js/va-client-register.js"></script>
|
||||||
<script>
|
</body>
|
||||||
function setTheme(theme) {
|
</html>
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
|
||||||
localStorage.setItem('theme', theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemTheme() {
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme
|
|
||||||
const savedTheme = getSystemTheme();
|
|
||||||
setTheme(savedTheme);
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
|
|
@ -1,124 +1,287 @@
|
||||||
<!-- Mobile Toggle Button -->
|
<!DOCTYPE html>
|
||||||
<div class="mobile-toggle" id="toggleBtn">
|
<html lang="en">
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile Overlay -->
|
<head>
|
||||||
<div id="overlay"></div>
|
<meta charset="UTF-8" />
|
||||||
|
<title>Live Screen Feed</title>
|
||||||
|
|
||||||
<div class="sidebar" id="sidebar">
|
<style>
|
||||||
<h3>Visible Apps</h3>
|
* {
|
||||||
<div id="appsList"></div>
|
box-sizing: border-box;
|
||||||
</div>
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
<div class="content">
|
|
||||||
<h2 id="selectedAppTitle">Select an app to view</h2>
|
|
||||||
<img id="liveImage" alt="Live Feed" />
|
|
||||||
<div id="placeholder" class="placeholder">No app selected</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
|
||||||
<script>
|
|
||||||
const vaId = "<%= vaId %>";
|
|
||||||
const clientId = "<%= clientId %>";
|
|
||||||
const projectId = "<%= projectId %>";
|
|
||||||
const token = localStorage.getItem("knoptpq");
|
|
||||||
|
|
||||||
const socket = io({
|
|
||||||
auth: {
|
|
||||||
token: token,
|
|
||||||
userType: "client",
|
|
||||||
projectId: projectId,
|
|
||||||
vaId: vaId
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.emit("register", {
|
|
||||||
role: "client",
|
|
||||||
id: vaId,
|
|
||||||
projectId: "<%= projectId %>",
|
|
||||||
clientId: clientId
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const sidebar = document.getElementById("sidebar");
|
|
||||||
const toggleBtn = document.getElementById("toggleBtn");
|
|
||||||
const overlay = document.getElementById("overlay");
|
|
||||||
|
|
||||||
const appsList = document.getElementById("appsList");
|
|
||||||
const liveImage = document.getElementById("liveImage");
|
|
||||||
const selectedAppTitle = document.getElementById("selectedAppTitle");
|
|
||||||
const placeholder = document.getElementById("placeholder");
|
|
||||||
|
|
||||||
let currentApp = null;
|
|
||||||
|
|
||||||
/* MOBILE SIDEBAR TOGGLE */
|
|
||||||
toggleBtn.onclick = () => {
|
|
||||||
sidebar.classList.add("open");
|
|
||||||
overlay.classList.add("show");
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.onclick = () => {
|
|
||||||
sidebar.classList.remove("open");
|
|
||||||
overlay.classList.remove("show");
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on("connected", () => {
|
|
||||||
socket.emit("requestVisibleApps", { token });
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Receive visible apps */
|
|
||||||
socket.on("receiveVisibleApps", ({ apps, vaId: senderVaId }) => {
|
|
||||||
if (vaId !== senderVaId) return;
|
|
||||||
|
|
||||||
appsList.innerHTML = "";
|
|
||||||
|
|
||||||
if (!apps?.length) {
|
|
||||||
appsList.innerHTML = "<p style='color:#777;'>No visible apps</p>";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apps.forEach(app => {
|
body {
|
||||||
const div = document.createElement("div");
|
height: 100vh;
|
||||||
div.classList.add("app");
|
display: flex;
|
||||||
div.textContent = app.name || "Unnamed App";
|
background: #0f0f0f;
|
||||||
|
color: white;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
div.onclick = () => {
|
/* SIDEBAR */
|
||||||
document.querySelectorAll(".app").forEach(el => el.classList.remove("active"));
|
.sidebar {
|
||||||
div.classList.add("active");
|
width: 280px;
|
||||||
|
background: #181818;
|
||||||
|
border-right: 1px solid #2b2b2b;
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: transform 0.35s ease;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
currentApp = app.name;
|
.sidebar h3 {
|
||||||
selectedAppTitle.textContent = app.name;
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
placeholder.style.display = "none";
|
.app {
|
||||||
liveImage.style.display = "none";
|
background: #252525;
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
socket.emit("selectApp", { appTitle: app.name, clientId, vaId, projectId });
|
.app:hover {
|
||||||
|
background: #333;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
// Close sidebar on mobile
|
.app.active {
|
||||||
sidebar.classList.remove("open");
|
background: #6c63ff;
|
||||||
overlay.classList.remove("show");
|
border-color: #6c63ff;
|
||||||
};
|
color: white;
|
||||||
|
box-shadow: 0 0 10px rgba(108, 99, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
appsList.appendChild(div);
|
/* MAIN CONTENT */
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#selectedAppTitle {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#liveImage {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #444;
|
||||||
|
object-fit: contain;
|
||||||
|
background: black;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #bbb;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MOBILE RESPONSIVE */
|
||||||
|
.mobile-toggle {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
background: #6C63FF;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 9999;
|
||||||
|
/* VERY HIGH: always visible */
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toggle span {
|
||||||
|
display: block;
|
||||||
|
width: 26px;
|
||||||
|
height: 3px;
|
||||||
|
background: #fff;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Hidden on Mobile */
|
||||||
|
@media (max-width: 780px) {
|
||||||
|
body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open {
|
||||||
|
transform: translateX(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-toggle {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark overlay when sidebar opens */
|
||||||
|
#overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: none;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
#overlay.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Mobile Toggle Button -->
|
||||||
|
<div class="mobile-toggle" id="toggleBtn">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Overlay -->
|
||||||
|
<div id="overlay"></div>
|
||||||
|
|
||||||
|
<div class="sidebar" id="sidebar">
|
||||||
|
<h3>Visible Apps</h3>
|
||||||
|
<div id="appsList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h2 id="selectedAppTitle">Select an app to view</h2>
|
||||||
|
<img id="liveImage" alt="Live Feed" />
|
||||||
|
<div id="placeholder" class="placeholder">No app selected</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
<script>
|
||||||
|
const vaId = "<%= vaId %>";
|
||||||
|
const clientId = "<%= clientId %>";
|
||||||
|
const projectId = "<%= projectId %>";
|
||||||
|
const token = localStorage.getItem("knoptpq");
|
||||||
|
|
||||||
|
const socket = io({
|
||||||
|
auth: {
|
||||||
|
token: token,
|
||||||
|
userType: "client",
|
||||||
|
projectId: projectId,
|
||||||
|
vaId: vaId
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("authError", () => {
|
socket.emit("register", {
|
||||||
socket.disconnect();
|
role: "client",
|
||||||
localStorage.removeItem("knoptpq");
|
id: vaId,
|
||||||
window.location.href = "/client/login";
|
projectId: "<%= projectId %>",
|
||||||
});
|
clientId: clientId
|
||||||
|
});
|
||||||
|
|
||||||
/* Receive images */
|
const sidebar = document.getElementById("sidebar");
|
||||||
socket.on("receiveImage", (base64, title) => {
|
const toggleBtn = document.getElementById("toggleBtn");
|
||||||
if (currentApp && title === currentApp) {
|
const overlay = document.getElementById("overlay");
|
||||||
liveImage.src = base64;
|
|
||||||
liveImage.style.display = "block";
|
const appsList = document.getElementById("appsList");
|
||||||
placeholder.style.display = "none";
|
const liveImage = document.getElementById("liveImage");
|
||||||
}
|
const selectedAppTitle = document.getElementById("selectedAppTitle");
|
||||||
});
|
const placeholder = document.getElementById("placeholder");
|
||||||
</script>
|
|
||||||
|
let currentApp = null;
|
||||||
|
|
||||||
|
/* MOBILE SIDEBAR TOGGLE */
|
||||||
|
toggleBtn.onclick = () => {
|
||||||
|
sidebar.classList.add("open");
|
||||||
|
overlay.classList.add("show");
|
||||||
|
};
|
||||||
|
|
||||||
|
overlay.onclick = () => {
|
||||||
|
sidebar.classList.remove("open");
|
||||||
|
overlay.classList.remove("show");
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Receive visible apps */
|
||||||
|
socket.on("receiveVisibleApps", ({apps, vaId: senderVaId}) => {
|
||||||
|
if (vaId !== senderVaId) return;
|
||||||
|
|
||||||
|
appsList.innerHTML = "";
|
||||||
|
|
||||||
|
if (!apps?.length) {
|
||||||
|
appsList.innerHTML = "<p style='color:#777;'>No visible apps</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
apps.forEach(app => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.classList.add("app");
|
||||||
|
div.textContent = app.name || "Unnamed App";
|
||||||
|
|
||||||
|
div.onclick = () => {
|
||||||
|
document.querySelectorAll(".app").forEach(el => el.classList.remove("active"));
|
||||||
|
div.classList.add("active");
|
||||||
|
|
||||||
|
currentApp = app.name;
|
||||||
|
selectedAppTitle.textContent = app.name;
|
||||||
|
|
||||||
|
placeholder.style.display = "none";
|
||||||
|
liveImage.style.display = "none";
|
||||||
|
|
||||||
|
socket.emit("selectApp", { appTitle: app.name, clientId, vaId, projectId });
|
||||||
|
|
||||||
|
// Close sidebar on mobile
|
||||||
|
sidebar.classList.remove("open");
|
||||||
|
overlay.classList.remove("show");
|
||||||
|
};
|
||||||
|
|
||||||
|
appsList.appendChild(div);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("authError", () => {
|
||||||
|
socket.disconnect();
|
||||||
|
localStorage.removeItem("knoptpq");
|
||||||
|
window.location.href = "/client/login";
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Receive images */
|
||||||
|
socket.on("receiveImage", (base64, title) => {
|
||||||
|
if (currentApp && title === currentApp) {
|
||||||
|
liveImage.src = base64;
|
||||||
|
liveImage.style.display = "block";
|
||||||
|
placeholder.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -1,4 +1,98 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Virtual Assistant Registration</title>
|
||||||
|
|
||||||
|
<link href="/public/css/va-register.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-registration {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-registration img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #343a40;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-outline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control-lg {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.7rem 2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background-color: #ffc107;
|
||||||
|
border: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background-color: #e0a800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-light {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
border-bottom: 2px solid #ffc107;
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.card-body {
|
||||||
|
padding: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-registration img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Sidebar for desktop -->
|
<!-- Sidebar for desktop -->
|
||||||
|
|
@ -77,28 +171,28 @@
|
||||||
<form id="account-form">
|
<form id="account-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="email" id="email" class="input-field" placeholder=" " value="<%= email %>" readonly disabled required>
|
<input type="email" id="email" class="input-field" placeholder=" " required autocomplete="off">
|
||||||
<label for="email" class="input-label">Email Address *</label>
|
<label for="email" class="input-label">Email Address *</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="new-password">
|
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="off">
|
||||||
<label for="password" class="input-label">Create Password *</label>
|
<label for="password" class="input-label">Create Password *</label>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="new-password">
|
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="off">
|
||||||
<label for="confirmPassword" class="input-label">Confirm Password *</label>
|
<label for="confirmPassword" class="input-label">Confirm Password *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
|
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</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>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|
@ -165,32 +259,21 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<select id="country" class="select-field" required onchange="handleChangeSelect(this)">
|
<select id="country" class="select-field" required>
|
||||||
<option value="" disabled selected></option>
|
<option value=""></option>
|
||||||
</select>
|
</select>
|
||||||
<label for="country" class="input-label">Country *</label>
|
<label for="country" class="input-label">Country *</label>
|
||||||
<!-- <div class="location-loading" id="country-loading" style="display: none;">Detecting your location...</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="state" class="input-field" placeholder=" " required>
|
<select id="state" class="select-field" required>
|
||||||
<!-- <select id="state" class="select-field" required onchange="handleChangeSelect(this)">
|
|
||||||
<option value=""></option>
|
<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-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<select id="status" class="select-field" required>
|
|
||||||
<option value="FULLTIME" selected>Full Time</option>
|
|
||||||
<option value="PARTTIME">Part Time</option>
|
|
||||||
</select>
|
</select>
|
||||||
<label for="status" class="input-label">Status *</label>
|
<label for="state" class="input-label">State / Region *</label>
|
||||||
|
<div class="location-loading" id="state-loading">Loading states...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -212,19 +295,19 @@
|
||||||
<label for="phone" class="input-label">Phone Number *</label>
|
<label for="phone" class="input-label">Phone Number *</label>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="tel" id="phone" class="input-field" placeholder=" " pattern="[0-9]*" minlength="10" maxlength="15" oninput="validatePhoneNumber(this)" required>
|
<input type="tel" id="phone" class="input-field" placeholder=" " pattern="[0-9]*" minlength="10" maxlength="15" oninput="validatePhoneNumber(this)" required>
|
||||||
<label for="phone" class="input-label">Phone Number</label>
|
<label for="phone" class="input-label">+ Phone Number</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="phone-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
<div class="invalid-feedback" id="phone-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
Please enter a valid phone number (10-15 digits)
|
Please enter a valid phone number (10-15 digits)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="skype" class="input-field" placeholder=" " oninput="validateSkypeId(this)">
|
<input type="text" id="skype" class="input-field" placeholder=" ">
|
||||||
<label for="skype" class="input-label">Skype ID</label>
|
<label for="skype" class="input-label">Skype ID</label>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
|
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
|
||||||
|
|
@ -240,40 +323,7 @@
|
||||||
<div class="error-message" id="step-4-error-message"></div>
|
<div class="error-message" id="step-4-error-message"></div>
|
||||||
</div>
|
</div>
|
||||||
<form id="payment-form">
|
<form id="payment-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<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="email" id="veemAccount" class="input-field" placeholder=" " oninput="validateVeemEmail(this)" required>
|
|
||||||
<label for="veemAccount" class="input-label">Veem Email Address *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Invalid Veem email address
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="payment-option" onclick="selectPaymentMethod('wise')">
|
|
||||||
<h4>Wise</h4>
|
|
||||||
<p>Receive payments via Wise</p>
|
|
||||||
</div>
|
|
||||||
<div id="wise-details" class="payment-details">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-container">
|
|
||||||
<input type="email" id="wiseEmail" class="input-field" placeholder=" " oninput="validateWiseEmail(this)" required>
|
|
||||||
<label for="wiseEmail" class="input-label">Wise Email Address *</label>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback" id="wise-email-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
|
||||||
Please enter a valid email address for Wise
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="form-title additional-info-heading">Additional Information</h2>
|
|
||||||
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
|
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
|
||||||
<h4>Payoneer</h4>
|
<h4>Payoneer</h4>
|
||||||
<p>Receive payments via Payoneer</p>
|
<p>Receive payments via Payoneer</p>
|
||||||
|
|
@ -281,11 +331,27 @@
|
||||||
<div id="payoneer-details" class="payment-details">
|
<div id="payoneer-details" class="payment-details">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="email" id="payoneerAccount" class="input-field" placeholder=" " oninput="validatePayoneerEmail(this)" required>
|
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" required>
|
||||||
<label for="payoneerAccount" class="input-label">Payoneer Email Address *</label>
|
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
Please enter a valid email address for Payoneer
|
Account number must be 5-20 characters
|
||||||
|
</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=" " minlength="8" maxlength="25" oninput="validateVeemAccount(this)" required>
|
||||||
|
<label for="veemAccount" class="input-label">Veem Account Number *</label>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
|
Account number must be 8-25 characters
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -311,18 +377,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/public/js/va-register.js"></script>
|
<script src="/public/js/va-register.js"></script>
|
||||||
<script>
|
</body>
|
||||||
function setTheme(theme) {
|
</html>
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
|
||||||
localStorage.setItem('theme', theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemTheme() {
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme
|
|
||||||
const savedTheme = getSystemTheme();
|
|
||||||
setTheme(savedTheme);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue