Compare commits

..

No commits in common. "master" and "merge2" have entirely different histories.

98 changed files with 3328 additions and 10790 deletions

3
.gitignore vendored
View File

@ -1,4 +1,3 @@
node_modules
logs
.env
public/builds
.env

View File

24
package-lock.json generated
View File

@ -20,7 +20,6 @@
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-ejs-layouts": "^2.5.1",
"express-minify": "^1.0.0",
"helmet": "^8.1.0",
"html-minifier": "^4.0.0",
@ -41,8 +40,7 @@
"uglify-js": "^3.19.3",
"uuid": "^11.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"wireguard-tools": "^0.1.0"
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@babel/cli": "^7.26.4",
@ -4233,11 +4231,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz",
@ -6205,12 +6198,6 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"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": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -9351,15 +9338,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",

View File

@ -6,7 +6,6 @@
"scripts": {
"start": "nodemon --inspect src/server.js",
"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",
"dev": "npx webpack --mode development"
},
@ -25,7 +24,6 @@
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-ejs-layouts": "^2.5.1",
"express-minify": "^1.0.0",
"helmet": "^8.1.0",
"html-minifier": "^4.0.0",
@ -46,8 +44,7 @@
"uglify-js": "^3.19.3",
"uuid": "^11.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"wireguard-tools": "^0.1.0"
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@babel/cli": "^7.26.4",

File diff suppressed because it is too large Load Diff

View File

@ -5,52 +5,9 @@
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 {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
background-color: #fff;
color: #333;
}
.container {
@ -64,12 +21,11 @@ body {
display: none;
flex-direction: column;
width: 388px;
background: var(--bg-sidebar);
background: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7);
padding: 24px;
border-radius: 14px;
margin: 20px;
box-shadow: var(--shadow);
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.logo {
@ -81,24 +37,18 @@ body {
width: 100%;
height: auto;
max-width: 196px;
filter: brightness(var(--logo-brightness, 1));
}
[data-theme="dark"] .logo-placeholder img {
--logo-brightness: 0.9;
}
.logo-placeholder h2 {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
color: #000;
text-align: center;
}
.form-title-sec {
font-size: 24px;
font-weight: 700;
color:var(--text-primary);
color: #000;
text-align: center;
margin-bottom: 24px;
}
@ -118,25 +68,15 @@ body {
border: 2px solid transparent;
cursor: pointer;
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 {
border-color: var(--accent-color);
}
[data-theme="dark"] .step.active {
background: rgba(255, 255, 255, 0.1);
border-color: #000000;
}
.step.completed .step-number {
background-color: var(--accent-color);
border-color: var(--accent-color);
background-color: #000000;
border-color: #000000;
color: white;
}
@ -147,43 +87,30 @@ body {
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--accent-color);
border: 2px solid #00000070;
font-weight: 600;
color: var(--accent-color);
color: #000000b5;
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 {
border-color:var(--text-primary);
color:var(--text-primary);
opacity: 1;
background: rgba(255, 255, 255, 0.9);
}
[data-theme="dark"] .step.active .step-number {
background: rgba(255, 255, 255, 0.2);
border-color: #000;
color: #000;
}
.step-text {
font-size: 14px;
color: var(--text-primary);
color: #000000b5;
}
.step.active .step-text {
color:var(--text-primary);
color: #000;
}
.step-connector {
height: 24px;
width: 2px;
background-color: var(--accent-color);
background-color: #00000070;
margin-left: 16px;
position: absolute;
top: 46px;
@ -191,7 +118,7 @@ body {
}
.step.active .step-connector {
background-color:var(--text-primary);
background-color: #000;
}
/* Main Content Styles */
@ -201,17 +128,15 @@ body {
flex-direction: column;
padding: 20px;
overflow-y: auto;
background-color: var(--bg-primary);
}
.mobile-progress {
background-color: var(--bg-secondary);
background-color: #f4f4f5;
padding: 16px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: var(--shadow);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow-x: auto;
border: 1px solid var(--border-color);
}
.mobile-progress ol {
@ -236,12 +161,11 @@ body {
.form-card {
width: 100%;
max-width: 450px;
background: var(--bg-card);
border: 1px solid var(--border-color);
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: var(--shadow-lg);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
padding: 24px;
transition: all 0.3s ease;
}
.form-title {
@ -249,7 +173,7 @@ body {
font-size: 20px;
font-weight: 600;
margin-bottom: 24px;
color:var(--text-primary);
color: #000;
}
.form-step {
@ -267,17 +191,10 @@ body {
.input-container {
position: relative;
background-color: var(--bg-secondary);
background-color: #f9fafb;
border-radius: 6px;
overflow: hidden;
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 {
@ -286,7 +203,7 @@ body {
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: var(--text-secondary);
color: #6b7280;
transition: all 0.2s ease;
pointer-events: none;
}
@ -299,16 +216,6 @@ body {
outline: none;
font-size: 14px;
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 {
@ -320,10 +227,6 @@ body {
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:not(:placeholder-shown) + .input-label,
.select-field:focus + .input-label,
@ -331,7 +234,6 @@ body {
top: 2px;
font-size: 12px;
transform: none;
color: var(--accent-light);
}
.checkbox-group {
@ -344,13 +246,10 @@ body {
.checkbox-group input {
width: 16px;
height: 16px;
accent-color: var(--accent-color);
}
.checkbox-group label {
font-size: 14px;
color: var(--text-primary);
cursor: pointer;
}
.form-actions {
@ -370,33 +269,31 @@ body {
}
.btn-primary {
background-color: var(--accent-color);
background-color: #000000;
color: white;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-secondary {
background-color: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
background-color: #f3f4f6;
color: #374151;
}
.btn-secondary:hover {
background-color: var(--border-color);
background-color: #e5e7eb;
}
.btn-outline {
background-color: transparent;
border: 1px solid var(--accent-light);
color: var(--accent-light);
border: 1px solid #070065;
color: #070065;
}
.btn-outline:hover {
background-color: var(--accent-light);
background-color: #070065;
color: white;
}
@ -404,7 +301,6 @@ body {
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn-sm {
@ -421,109 +317,31 @@ body {
}
.payment-option.selected {
border-color: var(--border-color);
transition: all 0.3s ease;
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;
border-color: #000000;
background-color: #f0f9ff;
}
.payment-option h4 {
margin-bottom: 8px;
font-size: 16px;
color: var(--text-primary);
}
.payment-option p {
font-size: 14px;
color: var(--text-secondary);
color: #6b7280;
}
.payment-details {
display: none;
margin: 4px 0 16px;
padding: 16px;
border: 1px solid var(--border-color);
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.3s ease;
background-color: #bdbdbd;
}
.payment-details.active {
display: block;
animation: slideDown 0.3s ease-out;
}
.success-message {
@ -533,25 +351,24 @@ body {
.success-icon {
font-size: 48px;
color: var(--success-color);
color: #10b981;
margin-bottom: 16px;
}
.success-message h3 {
font-size: 24px;
margin-bottom: 8px;
color: var(--text-primary);
}
.success-message p {
color: var(--text-secondary);
color: #6b7280;
margin-bottom: 24px;
}
.location-loading {
display: none;
font-size: 12px;
color: var(--text-secondary);
color: #6b7280;
margin-top: 4px;
margin-left: 12px;
position: absolute;
@ -568,17 +385,17 @@ body {
}
.profile-incomplete {
background: var(--warning-bg);
border: 1px solid var(--warning-border);
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
color: var(--warning-text);
color: #856404;
}
.password-strength {
height: 5px;
background: var(--bg-secondary);
background: #e9ecef;
border-radius: 5px;
margin-top: 5px;
overflow: hidden;
@ -592,7 +409,7 @@ body {
}
.password-weak {
background: var(--error-color);
background: #dc3545;
width: 25%;
}
@ -602,7 +419,7 @@ body {
}
.password-strong {
background: var(--success-color);
background: #28a745;
width: 100%;
}
@ -613,134 +430,38 @@ body {
margin-top: 8px;
}
.password-toggle, .password-generate, .detect-location {
.password-toggle {
background: none;
border: none;
color: var(--accent-light);
color: #070065;
cursor: pointer;
font-size: 12px;
transition: color 0.3s ease;
}
.password-toggle:hover, .password-generate:hover, .detect-location:hover {
color: var(--accent-color);
.password-generate {
background: none;
border: none;
color: #070065;
cursor: pointer;
font-size: 12px;
}
.form-text {
font-size: 12px;
color: var(--text-secondary);
color: #6b7280;
margin-top: 4px;
}
/* Error States */
.step-error-container {
background-color: rgba(220, 38, 38, 0.1);
border: 1px solid rgba(220, 38, 38, 0.3);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
animation: slideDown 0.3s ease-out;
.detect-location {
background: none;
border: none;
color: #070065;
cursor: pointer;
font-size: 12px;
text-decoration: underline;
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 */
@media (min-width: 1024px) {
.sidebar {
@ -756,35 +477,41 @@ body {
}
}
@media (max-width: 1023px) {
[data-theme="dark"] .mobile-progress {
background-color: var(--bg-card);
border: 1px solid var(--border-dark);
}
.mobile-progress .step {
background: rgba(255, 255, 255, 0.05);
}
[data-theme="dark"] .mobile-progress .step {
background: rgba(255, 255, 255, 0.1);
}
.step-error-container {
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
animation: slideDown 0.3s ease-out;
}
@media print {
[data-theme="dark"] {
--bg-primary: #ffffff;
--bg-card: #ffffff;
--text-primary: #000000;
--text-secondary: #333333;
--border-color: #cccccc;
.error-message {
color: #dc2626;
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 #dc2626 !important;
background-color: #fef2f2;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
.sidebar {
display: none;
}
.btn {
display: none;
to {
opacity: 1;
transform: translateY(0);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -166,7 +166,7 @@ function validateCVV(cvv, cardType) {
}
// ZIP code validation
function validateZipCodeRegex(zipCode) {
function validateZipCode(zipCode) {
return /^\d{5}(-\d{4})?$/.test(zipCode);
}
@ -450,7 +450,7 @@ async function validateStep(step) {
// ZIP code validation
const zipCode = document.getElementById('card-zip').value;
if (!validateZipCodeRegex(zipCode)) {
if (!validateZipCode(zipCode)) {
isValid = false;
showError('Please enter a valid ZIP code', 'card-zip', step);
return isValid;
@ -491,13 +491,10 @@ async function validateStep(step) {
}
} else if (step === 1) {
const email = document.getElementById('email').value;
const firstName = document.getElementById('first-name').value;
const lastName = document.getElementById('last-name').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!firstName && !lastName && !password && !confirmPassword) {
const companyName = document.getElementById('company-name').value;
if (!validateEmail(email) && !companyName) {
isValid = false;
showError('Fields are Required', null, step);
showError('Both Fields are Required', null, step);
let requiredFields1 = form.querySelectorAll('[required]');
requiredFields1.forEach(field => {
if (!field.value.trim()) {
@ -519,48 +516,9 @@ async function validateStep(step) {
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) {
isValid = false;
showError('Company Name is Required', 'company-name', step);
showError('Company / Client Name is Required', null, step);
return isValid;
}
@ -652,13 +610,10 @@ function getFormId(step) {
function saveStepData(step) {
switch(step) {
case 1:
formData.userDetails.firstName = document.getElementById('first-name').value;
formData.userDetails.lastName = document.getElementById('last-name').value;
formData.userDetails.name = document.getElementById('company-name').value;
formData.userDetails.email = document.getElementById('email').value;
formData.userDetails.password = document.getElementById('password').value;
break;
case 2:
formData.userDetails.name = document.getElementById('company-name').value;
formData.userDetails.address = document.getElementById('address').value;
formData.userDetails.city = document.getElementById('city').value;
formData.userDetails.state = document.getElementById('state').value;
@ -735,50 +690,17 @@ async function submitForm() {
saveStepData(currentStep);
// Save payment details based on selected method
let cardNumber = document.getElementById('card-number');
let expiryDate = document.getElementById('expiry-date');
let securityCode = document.getElementById('security-code');
let cardZip = document.getElementById('card-zip');
let bankName = document.getElementById('bank-name');
let accountNumber = document.getElementById('account-number');
let routingNumber = document.getElementById('routing-number');
let accountType = document.getElementById('account-type');
let accountHolderName = document.getElementById('account-holder-name');
if(cardNumber.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;
if (formData.userDetails.paymentMethod === 'card') {
formData.userDetails.cardNumber = document.getElementById('card-number').value;
formData.userDetails.expiryDate = document.getElementById('expiry-date').value;
formData.userDetails.securityCode = document.getElementById('security-code').value;
formData.userDetails.cardZip = document.getElementById('card-zip').value;
} else if (formData.userDetails.paymentMethod === 'bank') {
formData.userDetails.bankName = document.getElementById('bank-name').value;
formData.userDetails.accountNumber = document.getElementById('account-number').value;
formData.userDetails.routingNumber = document.getElementById('routing-number').value;
formData.userDetails.accountType = document.getElementById('account-type').value;
formData.userDetails.accountHolderName = document.getElementById('account-holder-name').value;
}
// Show loading state
@ -812,7 +734,7 @@ async function submitForm() {
nextStep(6);
setTimeout(() => {
window.location.href = '/';
window.location.href = '/client/login';
}, 1000);
} else {
// Handle server errors
@ -902,29 +824,28 @@ async function detectLocation() {
await populateStates(countryCode, 'state');
await populateStates(countryCode, 'billing-state');
// auto populate state if region matches
// const stateSelect = document.getElementById('state');
// const billingStateSelect = document.getElementById('billing-state');
const stateSelect = document.getElementById('state');
const billingStateSelect = document.getElementById('billing-state');
// if (stateSelect && region) {
// const matchedState = Array.from(stateSelect.options).find(
// opt => opt.text.toLowerCase() === region.toLowerCase()
// );
// if (matchedState) {
// stateSelect.value = matchedState.value;
// stateSelect.dispatchEvent(new Event('change'));
// }
// }
if (stateSelect && region) {
const matchedState = Array.from(stateSelect.options).find(
opt => opt.text.toLowerCase() === region.toLowerCase()
);
if (matchedState) {
stateSelect.value = matchedState.value;
stateSelect.dispatchEvent(new Event('change'));
}
}
// if (billingStateSelect && region) {
// const matchedBillingState = Array.from(billingStateSelect.options).find(
// opt => opt.text.toLowerCase() === region.toLowerCase()
// );
// if (matchedBillingState) {
// billingStateSelect.value = matchedBillingState.value;
// billingStateSelect.dispatchEvent(new Event('change'));
// }
// }
if (billingStateSelect && region) {
const matchedBillingState = Array.from(billingStateSelect.options).find(
opt => opt.text.toLowerCase() === region.toLowerCase()
);
if (matchedBillingState) {
billingStateSelect.value = matchedBillingState.value;
billingStateSelect.dispatchEvent(new Event('change'));
}
}
} catch (error) {
console.error('Error detecting location:', error);
@ -1286,181 +1207,7 @@ function preventNumbers(event) {
return false;
}
// Function to validate first 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 to validate company/client name
function validateCompanyName(input) {
const errorElement = document.getElementById('company-name-error');
const value = input.value;
@ -1515,7 +1262,7 @@ function validateCompanyNameContent(companyName) {
}
// Function to prevent invalid character input in real-time
function preventInvalidCompanyNameChars(event) {
function preventInvalidCompanyChars(event) {
const charCode = event.which ? event.which : event.keyCode;
const key = event.key;
@ -2174,52 +1921,4 @@ function preventInvalidAccountHolderChars(event) {
event.preventDefault();
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';
}
}
}

View File

@ -9,117 +9,69 @@
// Countries and states data
const countries = [
{ code: 'US', name: 'United States' },
{ code: 'CA', name: 'Canada' },
{ code: 'GB', name: 'United Kingdom' },
{ code: 'AU', name: 'Australia' },
{ code: 'IN', name: 'India' },
{ code: 'DE', name: 'Germany' },
{ code: 'FR', name: 'France' },
{ code: 'BR', name: 'Brazil' },
{ code: 'JP', name: 'Japan' },
{ code: 'CN', name: 'China' },
{ 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' }
{ code: 'US', name: 'United States' },
{ code: 'CA', name: 'Canada' },
{ code: 'GB', name: 'United Kingdom' },
{ code: 'AU', name: 'Australia' },
{ code: 'IN', name: 'India' },
{ code: 'DE', name: 'Germany' },
{ code: 'FR', name: 'France' },
{ code: 'BR', name: 'Brazil' },
{ code: 'JP', name: 'Japan' },
{ code: 'CN', name: 'China' }
];
// const states = {
// 'US': [
// { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
// { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
// { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
// { code: 'MI', name: 'Michigan' }
// ],
// 'CA': [
// { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
// { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
// ],
// 'GB': [
// { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
// { code: 'NIR', name: 'Northern Ireland' }
// ],
// 'AU': [
// { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
// { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
// ],
// 'IN': [
// { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
// { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
// ],
// 'DE': [
// { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
// { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
// ],
// 'FR': [
// { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
// { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
// { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
// ],
// 'BR': [
// { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
// { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
// ],
// 'JP': [
// { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
// { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
// ],
// 'CN': [
// { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
// { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
// ]
// };
const states = {
'US': [
{ code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
{ code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
{ code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
{ code: 'MI', name: 'Michigan' }
],
'CA': [
{ code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
{ code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
],
'GB': [
{ code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
{ code: 'NIR', name: 'Northern Ireland' }
],
'AU': [
{ code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
{ code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
],
'IN': [
{ code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
{ code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
],
'DE': [
{ code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
{ code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
],
'FR': [
{ code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
{ code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
{ code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
],
'BR': [
{ code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
{ code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
],
'JP': [
{ code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
{ code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
],
'CN': [
{ code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
{ code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
]
};
// Initialize the form
document.addEventListener('DOMContentLoaded', function() {
// Populate country dropdown
const countrySelect = document.getElementById('country');
countrySelect.innerHTML = '<option value="" disabled selected></option>';
countrySelect.innerHTML = '<option value=""></option>';
countries.forEach(country => {
const option = document.createElement('option');
@ -128,34 +80,32 @@
countrySelect.appendChild(option);
});
autoDetectCountry();
// Add event listener for country change
// countrySelect.addEventListener('change', function() {
// const countryCode = this.value;
// const stateSelect = document.getElementById('state');
countrySelect.addEventListener('change', function() {
const countryCode = this.value;
const stateSelect = document.getElementById('state');
// // Show loading
// document.getElementById('state-loading').style.display = 'block';
// Show loading
document.getElementById('state-loading').style.display = 'block';
// // Clear current states
// stateSelect.innerHTML = '<option value="" disabled selected></option>';
// Clear current states
stateSelect.innerHTML = '<option value=""></option>';
// // Simulate API delay
// setTimeout(() => {
// if (countryCode && states[countryCode]) {
// states[countryCode].forEach(state => {
// const option = document.createElement('option');
// option.value = state.code;
// option.textContent = state.name;
// stateSelect.appendChild(option);
// });
// }
// Simulate API delay
setTimeout(() => {
if (countryCode && states[countryCode]) {
states[countryCode].forEach(state => {
const option = document.createElement('option');
option.value = state.code;
option.textContent = state.name;
stateSelect.appendChild(option);
});
}
// // Hide loading
// document.getElementById('state-loading').style.display = 'none';
// }, 500);
// });
// Hide loading
document.getElementById('state-loading').style.display = 'none';
}, 500);
});
// 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 goToStep(step) {
// Only allow navigation to completed steps or the next step
@ -492,14 +346,6 @@
const email = document.getElementById('email').value;
const password = document.getElementById('password').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
if (!validateEmail(email)) {
@ -547,7 +393,7 @@
// Age validation
const age = document.getElementById('age').value;
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;
}
@ -615,35 +461,20 @@
showError('Please select a payment method', null, step);
isValid = false;
} else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) {
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false;
} else if (!validateEmail(payoneerAccount)) {
showError('Please enter a valid email address for Payoneer', 'payoneerAccount', step);
isValid = false;
}
} else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value;
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;
}
}
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) {
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false;
}
} else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) {
showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false;
}
}
}
}
@ -697,11 +528,10 @@
formData.userDetails.city = document.getElementById('city').value;
formData.userDetails.state = document.getElementById('state').value;
formData.userDetails.country = document.getElementById('country').value;
formData.status = document.getElementById('status').value;
break;
case 3:
formData.userDetails.phone = document.getElementById('phone').value;
// formData.userDetails.skype = document.getElementById('skype').value;
formData.userDetails.skype = document.getElementById('skype').value;
break;
case 4:
const selectedPayment = document.querySelector('.payment-option.selected');
@ -709,15 +539,11 @@
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
formData.userDetails.paymentMethod = paymentMethod;
if (document.getElementById('payoneerAccount').value) {
if (paymentMethod === 'payoneer') {
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
}
if (document.getElementById('veemAccount').value) {
} else if (paymentMethod === 'veem') {
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
}
if (paymentMethod === 'wise') {
formData.userDetails.wiseEmail = document.getElementById('wiseEmail').value;
}
}
break;
}
@ -743,74 +569,9 @@
document.getElementById('payoneer-details').style.display = 'block';
} else if (method === 'veem') {
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 generatePassword() {
const length = 12;
@ -981,13 +742,11 @@
// Validate age
if (age < 18) {
// 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', 'age');
showError('You Age must be 18 years or older to register', 'dob',);
ageInput.style.border = '1px solid #ef4444';
} else if (age > 100) {
// alert('Please enter a valid age');
// showError('Please enter a valid age', 'dob');
showError('Please enter a valid age', 'age');
showError('Please enter a valid age', 'dob');
ageInput.style.border = '1px solid #ef4444';
} else {
ageInput.style.border = '';
@ -1052,37 +811,18 @@
const payoneerError = document.getElementById('payoneer-error');
const value = input.value.trim();
// Remove any validation styling first
input.style.border = '';
payoneerError.style.display = 'none';
if (value.length > 0) {
// Check length
if (value.length < 5 || value.length > 20) {
payoneerError.textContent = 'Payoneer account must be 5-20 digits';
payoneerError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
}
// 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;
}
// Payoneer typically uses email format or alphanumeric IDs
// Adjust validation rules based on Payoneer's actual format requirements
if (value.length > 0 && (value.length < 5 || value.length > 20)) {
payoneerError.textContent = 'Payoneer account must be 5-20 characters';
payoneerError.style.display = 'block';
input.style.border = '1px solid #ef4445';
return false;
} else {
payoneerError.style.display = 'none';
input.style.border = '';
return true;
}
return true;
}
// Function to validate Veem account number
@ -1090,37 +830,17 @@
const veemError = document.getElementById('veem-error');
const value = input.value.trim();
// Remove any validation styling first
input.style.border = '';
veemError.style.display = 'none';
if (value.length > 0) {
// Check length
if (value.length < 8 || value.length > 25) {
veemError.textContent = 'Veem account must be 8-25 digits';
veemError.style.display = 'block';
input.style.border = '1px solid #ef4444';
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;
}
// Veem account numbers are typically longer alphanumeric strings
if (value.length > 0 && (value.length < 8 || value.length > 25)) {
veemError.textContent = 'Veem account must be 8-25 characters';
veemError.style.display = 'block';
input.style.border = '1px solid #ef4445';
return false;
} else {
veemError.style.display = 'none';
input.style.border = '';
return true;
}
return true;
}
function showError(message, fieldId = null, step = null) {
@ -1181,28 +901,4 @@
function getCurrentStep() {
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;
}
}
}

View File

@ -1,6 +1,5 @@
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken";
import getDayNumber from "../../../utils/getDayNumber.js";
// Getting client dashboard data - all VAs, projects, and active VAs
async function client_dashboard(req, res) {
@ -69,7 +68,12 @@ async function client_dashboard(req, res) {
$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);
// 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
const activityByVa = agg.reduce((acc, a) => {
acc[String(a._id)] = {
lastLog: a.lastLog,
totalLogins: a.totalLogins,
lastActivity: a.lastActivity
lastActivity: a.lastActivity,
projectsWorked: a.projectsWorked || []
};
return acc;
}, {});
@ -112,14 +126,14 @@ async function client_dashboard(req, res) {
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)) {
let totalWorkSeconds = 0;
// Group logs by date (YYYY-MM-DD format)
const logsByDate = {};
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]) {
logsByDate[date] = [];
}
@ -133,6 +147,7 @@ async function client_dashboard(req, res) {
let dayWorkSeconds = 0;
let lastLogin = null;
let lastProjectId = null;
// If day has adjustment logs, only process adjustment logs
// If no adjustment logs, process all logs for that day
@ -149,10 +164,14 @@ async function client_dashboard(req, res) {
if (log.loggedType === "LOGIN") {
lastLogin = time;
lastProjectId = log.projectId?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
// reset after each pair
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
const currentTime = new Date();
// Format all assigned VAs - Only include data needed for frontend
// Format all assigned VAs
const allVAsFormatted = allAssignedVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog;
const projectWorkTime = projectWorkTimeByVa[String(v._id)] || {};
// Calculate current session duration if still logged in
let currentSessionDuration = null;
let isCurrentlyActive = false;
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);
isCurrentlyActive = true;
// 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
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 {
id: v._id,
name: fullName,
@ -303,23 +231,102 @@ async function client_dashboard(req, res) {
status: v.status || 'active',
isCurrentlyActive: isCurrentlyActive,
lastLoginTime: lastLog?.time || null,
lastActivity: activity.lastActivity,
currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
totalLogins: activity.totalLogins || 0,
lastProject: currentProjectDetails,
lastProjectId: lastLog?.projectId || null,
projectsAssigned: clientProjectsAssigned.length,
// Detailed project information for popup
projects: projectsWithWorkTime,
projectsWorked: activity.projectsWorked || [],
projectsAssigned: v.projectIds?.length || 0,
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
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 inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive);
@ -327,22 +334,45 @@ async function client_dashboard(req, res) {
// Calculate total worked time across all VAs
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({
success: true,
data: {
// Only send data that frontend actually uses
allVAs: allVAsFormatted
// All assigned VAs with their status
allVAs: {
active: currentlyActiveVAs,
inactive: inactiveVAs,
total: allVAsFormatted
},
// Currently working VAs (active session)
activeVAs: activeVAsFormatted,
// All client projects
projects: projectsFormatted
},
summary: {
totalVAs: allAssignedVAs.length,
activeVAs: currentlyActiveVAs.length,
inactiveVAs: inactiveVAs.length,
totalProjects: clientProjects.length,
projectsWithActiveWorkers: projectsWithActiveWorkers,
totalHoursLogged: totalHoursLogged,
totalDedicatedHours: totalDedicatedHours,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
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) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);

View File

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

View File

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

View File

@ -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 getDayNumber from "../../../utils/getDayNumber.js";
function formatHoursToReadable(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) {
try {
const accessToken = req.headers.token;
@ -32,139 +31,64 @@ async function client_dashboard_projects(req, res) {
}
const clientEmail = decoded.email;
// STEP 1: Get client ID from token
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
if (!clientUser) {
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
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
// Get all VAs assigned to this client (with required fields only)
const allAssignedVAs = await VAModel.find({
_id: { $in: allProjectVaIds.vaId }
_id: { $in: clientVaIds }
})
.select('userDetails.firstName userDetails.lastName userDetails.email status')
.lean()
.exec();
// Create a map for quick VA lookup
const vaMap = allAssignedVAs.reduce((acc, va) => {
acc[va._id.toString()] = va;
return acc;
}, {});
// Get All Projects of client with required fields
const clientProjects = await ProjectModel.find({
_id: { $in: clientProjectIds }
})
.select('name hourlyRate vaAssigned dedicatedHours status')
.lean()
.exec();
// STEP 4: Check VA activity per project
const currentTime = new Date();
// Format projects with assigned VA details
const projectsFormatted = await Promise.all(clientProjects.map(async (project) => {
const assignedVAIds = project.vaAssigned || [];
// 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
};
// Format VAs with only required fields
const vasFormatted = allAssignedVAs.map((va) => ({
id: va._id,
firstName: va.userDetails?.firstName || '',
lastName: va.userDetails?.lastName || '',
email: va.userDetails?.email || '',
status: va.status || ''
}));
// Calculate summary counts
const totalProjects = projectsFormatted.length;
// Count total unique VAs across all projects
const allVAsAcrossProjects = new Set();
projectsFormatted.forEach(project => {
project.assignedVADetails.forEach(va => {
allVAsAcrossProjects.add(va.id);
});
});
// 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;
// Format projects with only required fields
const projectsFormatted = clientProjects.map((project) => ({
id: project._id,
name: project.name,
hourlyRate: project.hourlyRate,
vaAssigned: project.vaAssigned.length || 0,
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || 0,
status: project.status || ''
}));
return res.status(200).json({
success: true,
data: {
vas: vasFormatted,
projects: projectsFormatted
},
counts: {
totalProjects: totalProjects,
totalVAs: allVAsAcrossProjects.size,
activeVAs: totalActiveVAs.size,
projectsWithActiveVAs: projectsWithActiveVAs
totalVAs: vasFormatted.length,
totalProjects: projectsFormatted.length
}
});
} catch (error) {
console.error("Client dashboard projects error:", error);
console.error("Client dashboard error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error",

View File

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

View File

@ -1,325 +1,82 @@
import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.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 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) {
logger.info("ClientController - Register Client Hit ...");
try {
const payload = req.body || {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.lastName || !userDetails.password || !userDetails.name) {
return res.status(400).json({
success: false,
message: "Missing required fields: firstName, lastName, email, password, and company name are required"
if (!userDetails || !userDetails.email || !userDetails.name) {
return res.status(400).json({
success: false,
message: "Missing required fields: name and email are required"
});
}
const created = await vaClientService.registerClient(payload);
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
await passwordResetService.requestPasswordReset(userDetails.email, 'client');
logger.info(`Client password reset requested for: ${userDetails.email}`);
return res.status(201).json({
success: true,
return res.status(201).json({
success: true,
data: created,
message: "Client registered successfully"
message: "Client registered successfully"
});
} catch (error) {
logger.error(`ClientController - Register error: ${error.message}`);
if (error.message === "User already exists with this email") {
return res.status(409).json({
success: false,
message: error.message
return res.status(409).json({
success: false,
message: error.message
});
}
if (error.message.includes("Salesforce API is unreachable")) {
return res.status(503).json({
success: false,
message: "Salesforce service is currently unavailable. Please try again later."
return res.status(503).json({
success: false,
message: "Salesforce service is currently unavailable. Please try again later."
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error"
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
// Additional controller methods can be added here
async function get_client_profile(req, res) {
async function get_client_by_email(req, res) {
try {
const email = req.body.clientEmail;
const { email } = req.params;
const client = await vaClientService.getClientByEmail(email);
if (!client) {
return res.status(404).json({
success: false,
message: "Client not found"
return res.status(404).json({
success: false,
message: "Client not found"
});
}
return res.status(200).json({
success: true,
data: client
return res.status(200).json({
success: true,
data: client
});
} catch (error) {
logger.error(`ClientController - Get client error: ${error.message}`);
return res.status(500).json({
success: false,
message: "Internal Server Error"
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
async function delete_client(req, res) {
logger.info("ClientController - Delete Client Hit ...");
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
export {
va_register_client,
get_client_by_email
};

View File

@ -1,6 +1,6 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { ClientModel, VAModel } from "../../models/vaModel.js";
import { ClientModel } from "../../models/vaModel.js";
// Login VA
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" });
}
let isVa = false;
// Find user by email
let user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
const vaUser = await VAModel.findOne({ "userDetails.email": userDetails.email });
if (vaUser) {
isVa = true
user = vaUser
}
const user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" });
}
@ -58,7 +49,6 @@ async function va_login_client(req, res) {
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
userType: isVa ? "VA" : "CLIENT",
},
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
{ expiresIn: "30d" }
@ -79,7 +69,6 @@ async function va_login_client(req, res) {
name: user.userDetails.firstName,
email: user.userDetails.email,
},
userType: isVa ? "VA" : "CLIENT",
});
} catch (error) {
console.error("va_client_login_register error:", error);

View File

@ -1,17 +1,17 @@
import PasswordResetRepository from "../repository/passwordReset.repository.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";
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,
// })
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,
})
);
// VA: Request password reset
@ -55,50 +55,26 @@ async function render_reset_form(req, res) {
try {
const { token, userType } = req.query;
// ❌ Missing params
if (!token || !userType) {
return res.render('auth/reset-password-error', {
title: "Reset Password",
layout: 'layouts/auth',
error: 'Reset token or user type is missing',
userType: userType || 'va'
});
return res.render('auth/reset-password-error', { error: 'Reset token or user type is missing', userType: userType || 'va' });
}
// ✅ Validate token
// Validate token
const validation = await passwordResetService.validateResetToken(token, userType);
if (!validation.valid) {
return res.render('auth/reset-password-error', {
title: "Reset Password",
layout: 'layouts/auth',
error: validation.message,
userType: userType || 'va'
});
return res.render('auth/reset-password-error', { error: validation.message, userType: userType || 'va' });
}
// ✅ Render reset password form
return res.render('auth/reset-password-form', {
title: "Reset Password",
layout: 'layouts/auth',
token,
userType,
error: null
});
// Render reset form with token
res.render('auth/reset-password-form', { token, userType, error: null });
} catch (error) {
logger.error(`PasswordResetController - Render form error: ${error.message}`);
return res.render('auth/reset-password-error', {
title: "Reset Password",
layout: 'layouts/auth',
error: 'Failed to load reset form',
userType: 'va'
});
res.render('auth/reset-password-error', { error: 'Failed to load reset form' });
}
}
// Process password reset form submission (shared for both VA and Client)
async function process_reset(req, res) {
try {

View File

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

View File

@ -4,21 +4,21 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
// Register a new VA
async function project_add_sf(req, res) {
try {
const payload = req.body || {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
const payload = req.body|| {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload;
// Validate required fields
if (!name) {
return res.status(400).json({
success: false,
message: "Project name is required"
return res.status(400).json({
success: false,
message: "Project name is required"
});
}
if (!clientId) {
return res.status(400).json({
success: false,
message: "Client ID is required"
return res.status(400).json({
success: false,
message: "Client ID is required"
});
}
@ -27,62 +27,48 @@ async function project_add_sf(req, res) {
try {
clientObjectId = new Types.ObjectId(clientId);
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid Client ID format"
return res.status(400).json({
success: false,
message: "Invalid Client ID format"
});
}
// Validate and convert vaAssigned to array of ObjectIds
// let vaAssignedArray = [];
// if (vaAssigned) {
// if (Array.isArray(vaAssigned)) {
// try {
// vaAssignedArray = vaAssigned.map(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) {
// return res.status(400).json({
// success: false,
// message: "Invalid VA ID format in vaAssigned array"
// });
// }
// } else if (typeof vaAssigned === 'string') {
// // 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"
// });
// }
// }
// }
let vaAssignedArray = [];
if (vaAssigned) {
if (Array.isArray(vaAssigned)) {
try {
vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format in vaAssigned array"
});
}
} else if (typeof vaAssigned === 'string') {
// 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
const toCreate = {
name: name.trim(),
hourlyRate: hourlyRate || '0',
clientId: clientObjectId,
vaAssigned: vaAssigned,
dedicatedHours: dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"],
vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowed: isFlexibleAllowed || true,
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
vaAssigned: vaAssignedArray,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
};
const created = await ProjectModel.create(toCreate);
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
const relationshipUpdate = await updateEntityRelationships({
project: {
projectId: created._id,
@ -95,8 +81,36 @@ async function project_add_sf(req, res) {
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) {
console.error("project_add_sf error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });

View File

@ -10,25 +10,20 @@ async function update_project_sf(req, res) {
try {
const { projectId } = req.params;
const payload = req.body || {};
const {
name,
hourlyRate,
vaAssigned,
dedicatedHours,
const {
name,
hourlyRate,
vaAssigned,
dedicatedHours,
status,
totalLoggedHours,
vaWorkingScheduleStart,
vaWorkingScheduleEnd,
isFlexibleAllowed,
vaWorkingDelayAllowedStart,
vaWorkingDelayAllowedEnd
totalLoggedHours
} = payload;
// Validate projectId
if (!projectId) {
return res.status(400).json({
success: false,
message: "Project ID is required"
return res.status(400).json({
success: false,
message: "Project ID is required"
});
}
@ -36,18 +31,18 @@ async function update_project_sf(req, res) {
try {
projectObjectId = new Types.ObjectId(projectId);
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid Project ID format"
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"
return res.status(404).json({
success: false,
message: "Project not found"
});
}
@ -59,9 +54,9 @@ async function update_project_sf(req, res) {
// Update name if provided
if (name !== undefined) {
if (!name.trim()) {
return res.status(400).json({
success: false,
message: "Project name cannot be empty"
return res.status(400).json({
success: false,
message: "Project name cannot be empty"
});
}
updateData.name = name.trim();
@ -74,7 +69,7 @@ async function update_project_sf(req, res) {
// Update dedicated hours if provided
if (dedicatedHours !== undefined) {
updateData.dedicatedHours = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"];
updateData.dedicatedHours = parseFloat(dedicatedHours) || 0;
}
// Update total logged hours if provided
@ -85,81 +80,49 @@ async function update_project_sf(req, res) {
// Update status if provided
if (status !== undefined) {
if (!["ACTIVE", "INACTIVE"].includes(status)) {
return res.status(400).json({
success: false,
message: "Status must be either ACTIVE or INACTIVE"
return res.status(400).json({
success: false,
message: "Status must be either ACTIVE or INACTIVE"
});
}
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
if (vaAssigned !== undefined) {
let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
let newVaAssignedArray = [];
// Parse and validate VA IDs
if (vaAssigned && vaAssigned.length > 0) {
if (Array.isArray(vaAssigned)) {
try {
newVaAssignedArray = vaAssigned.map(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)
}
});
newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format in vaAssigned array"
return res.status(400).json({
success: false,
message: "Invalid VA ID format in vaAssigned array"
});
}
} else if (typeof vaAssigned === 'string') {
try {
newVaAssignedArray = [new Types.ObjectId(vaAssigned)];
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format"
return res.status(400).json({
success: false,
message: "Invalid VA ID format"
});
}
}
}
// updateData.vaAssigned = newVaAssignedArray;
updateData.vaAssigned = newVaAssignedArray;
// 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 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
if (vaToAdd.length > 0) {
@ -186,9 +149,9 @@ async function update_project_sf(req, res) {
// If no fields to update
if (Object.keys(updateData).length === 0) {
return res.status(400).json({
success: false,
message: "No valid fields to update"
return res.status(400).json({
success: false,
message: "No valid fields to update"
});
}
@ -198,7 +161,7 @@ async function update_project_sf(req, res) {
updateData,
{ new: true, runValidators: true }
).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 (vaAssigned !== undefined) {
@ -222,33 +185,33 @@ async function update_project_sf(req, res) {
} catch (error) {
console.error("update_project_sf error:", error);
// MongoDB duplicate key error
if (error.code === 11000) {
return res.status(400).json({
success: false,
message: "Project with this name already exists"
return res.status(400).json({
success: false,
message: "Project with this name already exists"
});
}
// MongoDB validation error
if (error.name === 'ValidationError') {
const errors = Object.values(error.errors).map(err => err.message);
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: "Validation error",
errors: errors
errors: errors
});
}
return res.status(500).json({
success: false,
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
error: error.message
});
}
}
export {
export {
update_project_sf
};

View File

@ -1,102 +1,101 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// function to close open work tracks and create logout
// async function closePreviousSession(vaId, lastLog) {
// try {
// const currentTime = new Date();
// // Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
// const openWorkTracks = await WorkTrackModel.find({
// vaId,
// logId: lastLog._id,
// $or: [
// { to: { $exists: false } },
// { totalTime: 0 },
// { to: null }
// ]
// });
async function closePreviousSession(vaId, lastLog) {
try {
const currentTime = new Date();
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
const openWorkTracks = await WorkTrackModel.find({
vaId,
logId: lastLog._id,
$or: [
{ to: { $exists: false } },
{ totalTime: 0 },
{ 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
// for (const workTrack of openWorkTracks) {
// const fromTime = new Date(workTrack.from);
// const totalTimeMs = currentTime - fromTime;
// Close all open work tracks
for (const workTrack of openWorkTracks) {
const fromTime = new Date(workTrack.from);
const totalTimeMs = currentTime - fromTime;
// // Update the work track with end time and total time
// workTrack.to = currentTime;
// workTrack.totalTime = totalTimeMs;
// await workTrack.save();
// Update the work track with end time and total time
workTrack.to = currentTime;
workTrack.totalTime = totalTimeMs;
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
// const logoutLog = await LoggedModel.create({
// loggedType: "LOGOUT",
// vaId,
// vaEmail: lastLog.vaEmail, // Use email from previous log
// projectId: lastLog.projectId,
// time: currentTime // Use same timestamp for consistent logging
// });
// Create a logout log entry
const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT",
vaId,
vaEmail: lastLog.vaEmail, // Use email from previous log
projectId: lastLog.projectId,
time: currentTime // Use same timestamp for consistent logging
});
// // console.log(`Created logout log: ${logoutLog._id}`);
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfDataSync = {
// "_id": logoutLog._id,
// "loggedType": "LOGOUT",
// "vaId": vaId,
// "vaEmail": lastLog.vaEmail,
// "projectId": lastLog.projectId,
// "time": logoutLog.time,
// }
// console.log(`Created logout log: ${logoutLog._id}`);
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfDataSync = {
// "_id": logoutLog._id,
// "loggedType": "LOGOUT",
// "vaId": vaId,
// "vaEmail": lastLog.vaEmail,
// "projectId": lastLog.projectId,
// "time": logoutLog.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(sfDataSync)
// });
// 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}`);
// }
// 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);
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
// return {
// closedWorkTracks: openWorkTracks.length,
// logoutLogId: logoutLog._id,
// SyncSalesforce: result
// };
// } catch (error) {
// console.error("Failed to sync with Salesforce:", error.message);
// console.log(error)
// return {
// closedWorkTracks: openWorkTracks.length,
// 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;
// }
// // Create a custom error to distinguish Salesforce errors
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
// salesforceError.isSalesforceError = true;
// throw salesforceError;
// }
// // return {
// // closedWorkTracks: openWorkTracks.length,
// // logoutLogId: logoutLog._id,
// // };
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
};
// } catch (error) {
// console.error("Error closing previous session:", error);
// throw error;
// }
// }
} catch (error) {
console.error("Error closing previous session:", error);
throw error;
}
}
// Login VA
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
// Check if VA has any active sessions (LOGIN without LOGOUT)
// const activeSessions = await LoggedModel.find({
// vaId: user._id
// }).sort({ time: -1 });
const activeSessions = await LoggedModel.find({
vaId: user._id
}).sort({ time: -1 });
// let closeResult = [];
let closeResult = [];
// if (activeSessions.length > 0) {
// // Get the latest login to check if there's no corresponding logout
// const latestLog = activeSessions[0];
// closeResult = await closePreviousSession(user._id, latestLog);
// }
if (activeSessions.length > 0) {
// Get the latest login to check if there's no corresponding logout
const latestLog = activeSessions[0];
closeResult = await closePreviousSession(user._id, latestLog);
}
// Generate JWT accessToken with 24h expiry
const accessToken = jwt.sign(
@ -172,23 +171,23 @@ async function va_login_callback(req, res) {
let responseData = [];
// if (closeResult) {
// responseData = {
// success: true,
// message: "Login Successfull",
// accessToken,
// refreshToken,
// user: {
// id: user._id,
// name: user.userDetails.firstName,
// email: user.userDetails.email,
// },
// previousSession: {
// closedWorkTracks: closeResult.closedWorkTracks,
// logoutLogId: closeResult.logoutLogId
// }
// }
// } else {
if (closeResult) {
responseData = {
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
},
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId
}
}
} else {
responseData = {
success: true,
message: "Login Successfull",
@ -200,7 +199,7 @@ async function va_login_callback(req, res) {
email: user.userDetails.email,
}
}
// }
}
// Return accessToken and user info
return res.status(200).json(responseData);

View File

@ -1,12 +1,11 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Apply for leave
async function va_leave_apply_function(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
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;
if (!dateFrom || !dateTo || !leaveType || !comment) {
return res.status(400).json({
success: false,
message: "dateFrom, dateTo, leaveType, and comment are required"
return res.status(400).json({
success: false,
message: "dateFrom, dateTo, leaveType, and comment are required"
});
}
@ -37,49 +36,49 @@ async function va_leave_apply_function(req, res) {
try {
startDate = new Date(dateFrom);
endDate = new Date(dateTo);
// Set to start of day for accurate calculation
startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return res.status(400).json({
success: false,
message: "Invalid date format. Use valid date format (YYYY-MM-DD)"
return res.status(400).json({
success: false,
message: "Invalid date format. Use valid date format (YYYY-MM-DD)"
});
}
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid date format"
return res.status(400).json({
success: false,
message: "Invalid date format"
});
}
// Ensure start date is before or equal to end date
if (startDate > endDate) {
return res.status(400).json({
success: false,
message: "Start date must be before or equal to end date"
return res.status(400).json({
success: false,
message: "Start date must be before or equal to end date"
});
}
// Ensure dates are not in the past
const today = new Date();
today.setHours(0, 0, 0, 0);
if (startDate < today) {
return res.status(400).json({
success: false,
message: "Cannot apply for leave for past dates"
return res.status(400).json({
success: false,
message: "Cannot apply for leave for past dates"
});
}
// Validate leave type
const validLeaveTypes = ["SICK", "PTO"];
if (!validLeaveTypes.includes(leaveType)) {
return res.status(400).json({
success: false,
message: "Invalid leave type. Must be SICK or PTO"
return res.status(400).json({
success: false,
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
if (totalDays <= 0) {
return res.status(400).json({
success: false,
message: "Invalid date range"
return res.status(400).json({
success: false,
message: "Invalid date range"
});
}
@ -107,9 +106,9 @@ async function va_leave_apply_function(req, res) {
});
if (existingLeave) {
return res.status(400).json({
success: false,
message: "You already have a pending leave request that overlaps with these dates"
return res.status(400).json({
success: false,
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) {
return res.status(400).json({
success: false,
message: "You already have an approved leave that overlaps with these dates"
return res.status(400).json({
success: false,
message: "You already have an approved leave that overlaps with these dates"
});
}
const processedComment = processHTMLComment(comment);
// Create leave request
const leaveRequest = new LeavesModel({
vaId,
@ -140,74 +138,72 @@ async function va_leave_apply_function(req, res) {
dateTo: endDate,
days: totalDays.toString(),
leaveType,
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
comment,
status: "PENDING"
});
await leaveRequest.save();
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/leave-request`;
const sfData = {
"leaveId": leaveRequest._id.toString(),
// "va": {
// "id": vaId
// },
"va": vaId.toString(),
"dateFrom": startDate.toISOString().split('T')[0],
"dateTo": endDate.toISOString().split('T')[0],
"totalDays": totalDays,
"leaveType": leaveType,
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
"attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING",
"rejectionReason": ""
}
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
// const sfData = {
// "id": adjustmentRequest._id,
// "va": {
// "id": vaId
// },
// "project": {
// "id": projectId
// },
// "date": adjustmentDate.toISOString().split('T')[0],
// "loginTime": loginDateTime.toISOString(),
// "logoutTime": logoutDateTime.toISOString(),
// "comment": comment,
// "status": "PENDING",
// "rejectionReason": "",
// "adjustedHours": formatTime(adjustedSeconds)
// }
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({ "data": sfData })
});
// 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({"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 errorText = await response.text();
await LeavesModel.findByIdAndDelete(leaveRequest._id)
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: "Leave request submitted successfully",
// data: {
// leaveId: leaveRequest._id,
// dateFrom: startDate.toISOString().split('T')[0],
// dateTo: endDate.toISOString().split('T')[0],
// totalDays: totalDays,
// leaveType,
// comment: processedComment.processedHTML,
// // attachments: processedComment.extractedImages,
// status: "PENDING"
// },
// // 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;
}
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
// return res.status(201).json({
// success: true,
// message: "Adjustment request submitted successfully",
// data: {
// adjustmentId: adjustmentRequest._id,
// date: adjustmentDate.toISOString().split('T')[0],
// loginTime: loginDateTime.toISOString(),
// logoutTime: logoutDateTime.toISOString(),
// adjustedHours: formatTime(adjustedSeconds),
// comment: comment,
// status: "PENDING"
// },
// 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 res.status(201).json({
success: true,
@ -225,133 +221,21 @@ async function va_leave_apply_function(req, res) {
} catch (error) {
console.error("Leave apply error:", error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: "Invalid token" });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: "Token expired" });
}
return res.status(500).json({
success: false,
return res.status(500).json({
success: false,
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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#x2F;/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 };

View File

@ -5,10 +5,10 @@ import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../mod
function getDayRange(date = new Date()) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return { start, end };
}
@ -16,7 +16,7 @@ function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} else if (wholeHours > 0) {
@ -26,274 +26,245 @@ function formatHoursToReadable(hours) {
}
}
// // Helper function to check daily hours and create logout log if needed
// async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
// try {
// const today = new Date();
// const { start, end } = getDayRange(today);
// Helper function to check daily hours and create logout log if needed
async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
try {
const today = new Date();
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
// // Exclude the current session if it's already in the database
// const query = {
// vaId,
// projectId,
// from: { $gte: start, $lte: end }
// };
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": logoutLog.time,
}
// if (currentSessionId) {
// query._id = { $ne: currentSessionId };
// }
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 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
// });
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfDataSync = {
// "_id": logoutLog._id,
// "loggedType": "LOGOUT",
// "vaId": vaId,
// "vaEmail": vaEmail,
// "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 };
// }
// }
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
async function va_log_track_work_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 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 accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const vaUser = await VAModel.findById(vaId);
if (!vaUser) return res.status(400).json({ success: false, message: "VA not found" });
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
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" });
const logEntry = await LoggedModel.findById(logId);
if (!logEntry) return res.status(400).json({ success: false, message: "Log entry not found" });
if (logs.length === 0) {
return res.status(400).json({ success: false, message: "No logs to track" });
// 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",
});
}
logs[0].from = logEntry.time; // Ensure the first log's 'from' matches the main log time
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",
// });
// }
if (!projectId || !logId || !title || !appName) {
return res.status(400).json({ success: false, message: "Missing Props" });
}
// Handle POST → create entry
// if (req.method === "POST") {
// const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
// const createdTracks = await Promise.all(
// logs.map(async (log) => {
// // Calculate total time from timestamps
// const fromTime = new Date(log.from);
// const toTime = new Date(log.to);
// const totalTime = Math.abs(toTime - fromTime);
// 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);
if (req.method === "POST") {
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
if (dailyCheck.shouldLogout) {
return res.status(400).json({
success: false,
message: `Cannot start work - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`,
autoLogout: true
});
}
const createdTrack = await WorkTrackModel.create({
vaId,
projectId,
logId,
from: logs[0].from,
to: logs[logs.length -1].to,
totalTime: totalLogTime,
logs: logs
title,
appName,
from: Date.now(),
});
return res.status(201).json({
success: true,
message: "Work tracking created",
data: createdTrack
message: "Work tracking started",
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)
// if (req.method === "PATCH") {
// const { id } = payload; // expecting the workTrack record ID to update
// if (!id) {
// return res.status(400).json({ success: false, message: "Missing workTrack ID" });
// }
// Handle PATCH → update entry (add 'to' and calculate total time)
if (req.method === "PATCH") {
const { id } = payload; // expecting the workTrack record ID to update
if (!id) {
return res.status(400).json({ success: false, message: "Missing workTrack ID" });
}
// const workEntry = await WorkTrackModel.findById(id);
// if (!workEntry) {
// return res.status(404).json({ success: false, message: "Work entry not found" });
// }
const workEntry = await WorkTrackModel.findById(id);
if (!workEntry) {
return res.status(404).json({ success: false, message: "Work entry not found" });
}
// const toTime = new Date();
// const fromTime = new Date(workEntry.from);
// const totalTimeMs = toTime - fromTime;
const toTime = new Date();
const fromTime = new Date(workEntry.from);
const totalTimeMs = toTime - fromTime;
// // Optional: store totalTime in minutes or hours instead of raw ms
// const hours = Math.floor(totalTimeMs / (1000 * 60 * 60));
// const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60));
// const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000);
// Optional: store totalTime in minutes or hours instead of raw ms
const hours = Math.floor(totalTimeMs / (1000 * 60 * 60));
const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60));
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
// const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
// Check if daily dedicated hours are completed after this work session
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
// workEntry.to = toTime;
// workEntry.totalTime = totalTimeMs; // store as number of minutes
// await workEntry.save();
workEntry.to = toTime;
workEntry.totalTime = totalTimeMs; // store as number of minutes
await workEntry.save();
// const response = {
// success: true,
// message: "Work tracking stopped",
// data: {
// id: workEntry._id,
// from: workEntry.from,
// to: workEntry.to,
// formattedTotalTime,
// totalTimeMs,
// sessionHours: totalTimeMs / (1000 * 60 * 60)
// },
// dailyHours: {
// todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
// dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
// remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
// }
// };
const response = {
success: true,
message: "Work tracking stopped",
data: {
id: workEntry._id,
from: workEntry.from,
to: workEntry.to,
formattedTotalTime,
totalTimeMs,
sessionHours: totalTimeMs / (1000 * 60 * 60)
},
dailyHours: {
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
}
};
// // If auto-logout was triggered, add that info to response
// if (dailyCheck.shouldLogout) {
// response.autoLogout = true;
// response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
// response.logoutLogId = dailyCheck.logoutLogId;
// }
// If auto-logout was triggered, add that info to response
if (dailyCheck.shouldLogout) {
response.autoLogout = true;
response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
response.logoutLogId = dailyCheck.logoutLogId;
}
// return res.status(200).json(response);
// }
return res.status(200).json(response);
}
// 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) {
return res.status(500).send("Internal Server Error");
}

View File

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

View File

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

View File

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

View File

@ -5,96 +5,52 @@ import { VAModel } from "../../../models/vaModel.js";
// Register a new VA
async function va_profile_update_callback(req, res) {
try {
const sf_token = req.headers["x-auth-token"];
const accessToken = req.headers.token;
let vaUser;
let authSource = 'regular';
if(sf_token) {
try {
const sfDecoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
if (!sfDecoded?.email && !sfDecoded?.portalId) {
return res.status(400).json({ success: false, message: "Invalid Salesforce token payload" });
}
// Find user by email or VA ID from Salesforce token
if (sfDecoded.portalId) {
vaUser = await VAModel.findById(sfDecoded.portalId);
} else if (sfDecoded.email) {
vaUser = await VAModel.findOne({ "userDetails.email": sfDecoded.email });
}
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found with Salesforce token" });
}
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" });
}
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing access token" });
}
let decoded;
try {
decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
} catch (err) {
return res.status(401).json({ success: false, message: "Invalid or expired token" });
}
if (!decoded?.email) {
return res.status(400).json({ success: false, message: "Invalid token payload" });
}
const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const payload = req.body || {};
const { userDetails, status } = payload;
const { userDetails } = payload;
if (!userDetails) {
return res.status(400).json({ success: false, message: "Missing userDetails" });
}
const allowedFields = [
"firstName", "lastName", "dob", "age", "address",
"city", "state", "country", "phone", "skype",
"paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail"
];
// Allowed fields to update
// const allowedFields = [
// "firstName", "lastName", "dob", "age", "address",
// "bankName", "accountHolderName", "accountNumber", "routingNumber"
// ];
let hasUpdates = false;
const updatedFields = [];
// Update allowed fields if provided
allowedFields.forEach(field => {
if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') {
vaUser.userDetails[field] = userDetails[field];
hasUpdates = true;
updatedFields.push(field);
}
});
// // Update allowed fields only
// allowedFields.forEach(field => {
// if (userDetails[field] !== undefined) {
// vaUser.userDetails[field] = userDetails[field];
// }
// });
/* ---------------------- PASSWORD UPDATE ---------------------- */
let passwordUpdated = false;
if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) {
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) {
return res.status(400).json({
success: false,
@ -122,21 +78,11 @@ async function va_profile_update_callback(req, res) {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
vaUser.userDetails.password = hashedPassword;
hasUpdates = true;
passwordUpdated = true;
updatedFields.push('password');
}
if (!hasUpdates) {
} else {
return res.status(400).json({
success: false,
message: "No valid fields to update",
});
}
if (status) {
vaUser.status = status;
updatedFields.push('status');
success: false,
message: "Data not found",
});
}
await vaUser.save();
@ -149,7 +95,6 @@ async function va_profile_update_callback(req, res) {
success: true,
message: "Profile updated successfully",
data: updatedUser,
updatedFields: updatedFields,
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });

View File

@ -1,6 +1,5 @@
import bcrypt from "bcrypt";
import { VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Register a new VA
async function va_register(req, res) {
@ -37,7 +36,6 @@ async function va_register(req, res) {
paymentMethod: userDetails.paymentMethod || null,
payoneerAccount: userDetails.payoneerAccount || null,
veemAccount: userDetails.veemAccount || null,
wiseEmail: userDetails.wiseEmail || null,
phone: userDetails.phone || 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`;
delete created.userDetails._id;
created.userDetails._id = created._id; // Include VA's MongoDB ID
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -75,7 +73,7 @@ async function va_register(req, res) {
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
} catch (error) {
console.error("❌ Failed to sync with Salesforce:", error.message);
await VAModel.findOneAndDelete({_id: created.userDetails._id});
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;

View File

@ -1,16 +1,13 @@
import mongoose from "mongoose";
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
import getDayNumber from "../../../utils/getDayNumber.js";
import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.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()) {
const start = new Date(date);
start.setUTCHours(0, 0, 0, 0);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setUTCHours(23, 59, 59, 999);
end.setHours(23, 59, 59, 999);
return { start, end };
}
@ -18,7 +15,7 @@ function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} 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
async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
async function closePreviousSession(vaId, lastLog) {
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)
const openWorkTracks = await WorkTrackModel.find({
vaId,
@ -163,22 +40,22 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
{ to: null }
]
});
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
// Close all open work tracks
for (const workTrack of openWorkTracks) {
const fromTime = new Date(workTrack.from);
const totalTimeMs = currentTime - fromTime;
// Update the work track with end time and total time
workTrack.to = currentTime;
workTrack.totalTime = totalTimeMs;
await workTrack.save();
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
}
// Create a logout log entry
const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT",
@ -187,20 +64,20 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
projectId: lastLog.projectId,
time: currentTime // Use same timestamp for consistent logging
});
// console.log(`Created logout log: ${logoutLog._id}`);
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId,
"time": logoutLog.time,
}
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId,
"time": logoutLog.time,
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -209,7 +86,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
},
body: JSON.stringify(sfDataSync)
});
if (!response.ok) {
const errorText = await response.text();
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();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
@ -226,13 +103,13 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
} 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) {
console.error("Error closing previous session:", error);
throw error;
@ -243,202 +120,36 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
async function va_project_log_callback(req, res) {
try {
const payload = req.body || {};
const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload;
// console.log("Received payload:", payload);
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 { vaId, vaEmail, projectId, type } = payload;
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
const project = await ProjectModel.findById(projectId);
if (!project) {
return res.status(400).json({ success: false, message: "Project not found" });
}
const vaUser = await VAModel.findById(vaId);
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)
// For LOGIN requests, check daily hours
if (type === "LOGIN") {
const { start, end } = getDayRange(); // UTC day range
const { start, end } = getDayRange();
// Get today's total hours
const todayWorkEntries = await WorkTrackModel.find({
vaId,
projectId,
from: { $gte: start, $lte: end }
});
const totalTodayMs = todayWorkEntries.reduce((total, entry) => {
return total + (entry.totalTime || 0);
}, 0);
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
const dayNumber = getDayNumber(logTime);
// 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({
success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours[dayNumber])})`,
// warnings: scheduleCheck.warnings
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`
});
}
@ -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
// 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}`);
// Now proceed to create new login
const createdLog = await LoggedModel.create({
loggedType: "LOGIN",
vaId,
vaEmail,
projectId,
time: logTime
const createdLog = await LoggedModel.create({
loggedType: "LOGIN",
vaId,
vaEmail,
projectId
});
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
"_id": createdLog._id,
"loggedType": "LOGIN",
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": createdLog.time,
return res.status(201).send({
success: true,
message: `Login Successful (previous session closed)`,
logId: createdLog._id,
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId
}
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({
loggedType: type,
vaId,
vaEmail,
projectId,
time: logTime
});
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
// console.log(createdLog)
// 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,
// }
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 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(sfData)
// });
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
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)
});
// 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 });
// } 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;
// }
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,
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;
}
return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) {
console.error("Error in va_project_log_callback:", error);
return res.status(500).send("Internal Server Error");
}
}

View File

@ -1,6 +1,5 @@
import jwt from "jsonwebtoken";
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
import getDayNumber from "../../../utils/getDayNumber.js";
function formatHMS(ms) {
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());
projectList = await ProjectModel.find({ "vaAssigned.vaId": vaUser._id });
projectList = await ProjectModel.find({ vaAssigned: vaUser._id });
}
if (!projectList.length) {
@ -111,15 +110,13 @@ async function va_projects_log_list_function(req, res) {
const lastLog = logs[logs.length - 1];
const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY";
const dayNumber = getDayNumber();
finalReport.push({
projectId: project._id,
projectName: project.name,
activityStatus,
totalWorkTime: formatHMS(totalWorkMs),
totalWorkMs,
dedicatedHours: formatHoursToReadable(project?.dedicatedHours[dayNumber]) || "0h",
dedicatedHours: formatHoursToReadable(project?.dedicatedHours),
totalLogs: logs.length,
status: project?.status
});

View File

@ -11,36 +11,23 @@ function formatHMS(ms) {
async function va_projects_report_function(req, res) {
try {
const sf_token = req.headers["x-auth-token"];
let vaId;
let vaUser;
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 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 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;
if (!projectId) {
@ -49,38 +36,9 @@ async function va_projects_report_function(req, res) {
// Default: current day range
const now = new Date();
let fromDate, toDate;
if (from && from !== "undefined" && from !== "null") {
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"
});
}
const fromDate = from && from !== "undefined" ? new Date(from) : new Date(now.setHours(0, 0, 0, 0));
const toDate = to && to !== "undefined" ? new Date(to) : new Date();
toDate.setHours(23, 59, 59, 999);
// Get all VA login/logout logs for this project in range
const logs = await LoggedModel.find({
@ -90,35 +48,8 @@ async function va_projects_report_function(req, res) {
}).sort({ time: 1 }).lean();
if (!logs.length) {
return res.json({
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))
}
});
return res.json({ success: true, message: "No logs found for this project", data: [] });
}
// 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
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 = [];
for (const currentVaId of vaIds) {
const vaLogList = logs.filter(l => l.vaId.toString() === currentVaId);
for (const vaId of vaIds) {
const vaLogList = logs.filter(l => l.vaId.toString() === vaId);
let sessions = [];
let currentLogin = null;
const vaWorkTracks = workTracks.filter(w => w.vaId.toString() === currentVaId);
// Pair login → logout
for (const entry of vaLogList) {
if (entry.loggedType === "LOGIN") {
currentLogin = entry;
} 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;
}
}
// If still logged in, close at end of day
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;
const sessionDetails = [];
// for (const s of sessions) {
// const workData = await WorkTrackModel.find({
// vaId,
// projectId,
// from: { $gte: s.from },
// to: { $lte: s.to }
// }).sort({ from: 1 }).lean();
for (const s of sessions) {
const workData = await WorkTrackModel.find({
vaId,
projectId,
from: { $gte: s.from },
to: { $lte: s.to }
}).sort({ from: 1 }).lean();
// const tabs = workData.map(t => ({
// title: t.title,
// appName: t.appName,
// from: t.from,
// to: t.to,
// durationMs: t.totalTime,
// durationFormatted: formatHMS(t.totalTime),
// }));
const tabs = workData.map(t => ({
title: t.title,
appName: t.appName,
from: t.from,
to: t.to,
durationMs: t.totalTime,
durationFormatted: formatHMS(t.totalTime),
}));
// const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0);
// 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;
const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0);
totalWorkMs += sessionWorkMs;
sessionDetails.push({
from: session.from,
to: session.to,
totalSessionTime: formatHMS(sessionTotalMs),
totalSessionMs: sessionTotalMs,
workTracksCount: sessionWorkTracks.length,
tabs: sessionTabs.sort((a, b) => a.from - b.from), // Sort tabs by time
from: s.from,
to: s.to,
totalSessionTime: formatHMS(sessionWorkMs),
tabs,
});
}

View File

@ -1,24 +1,15 @@
import mongoose from "mongoose";
import { AdjustedHoursModel } from "../../../models/vaModel.js";
// Get all adjustment requests (with filtering options)
async function get_adjustment_requests_list(req, res) {
try {
const { status, projectId, vaId } = req.query;
const { status, projectId, vaId } = req.params;
// Build filter object
const filter = {};
// if (status) filter.status = status;
// if (projectId) filter.projectId = projectId;
// 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);
}
if (status) filter.status = status;
if (projectId) filter.projectId = projectId;
if (vaId) filter.vaId = vaId;
const adjustments = await AdjustedHoursModel.find(filter)
.sort({ createdAt: -1 })
@ -63,9 +54,9 @@ async function get_adjustment_requests_list(req, res) {
} catch (error) {
console.error("Get adjustment requests error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error"
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}

View File

@ -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
const loginLog = new LoggedModel({
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
adjustment.status = "REJECTED";
adjustment.reviewedAt = new Date();

View File

@ -1,6 +1,5 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Adjust missing hours by creating new records
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
const project = await ProjectModel.findOne({
_id: projectId,
"vaAssigned.vaId": vaId
vaAssigned: vaId
});
if (!project) {
@ -60,6 +59,14 @@ async function va_timesheet_adjustment_function(req, res) {
const combineDateTime = (dateObj, timeStr) => {
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);
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({
vaId,
projectId,
date: startOfDay,
loginTime: loginDateTime,
logoutTime: logoutDateTime,
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
comment,
status: "PENDING"
});
@ -139,24 +143,23 @@ async function va_timesheet_adjustment_function(req, res) {
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
const sfData = {
"id": adjustmentRequest._id.toString(),
"id": adjustmentRequest._id,
"va": {
"id": vaId.toString()
"id": vaId
},
"project": {
"id": projectId.toString()
"id": projectId
},
"date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(),
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
"attachments": processedComment.extractedImages, // Send extracted images info
"comment": comment,
"status": "PENDING",
"rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds)
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -168,7 +171,6 @@ async function va_timesheet_adjustment_function(req, res) {
if (!response.ok) {
const errorText = await response.text();
await AdjustedHoursModel.findByIdAndDelete(adjustmentRequest._id)
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(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
comment: comment,
status: "PENDING"
},
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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#x2F;/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 };

View File

@ -1,40 +1,26 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
import getDayNumber from "../../../utils/getDayNumber.js";
async function va_timesheet_summary_function(req, res) {
try {
const sf_token = req.headers["x-auth-token"];
let vaId;
let vaUser;
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 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 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;
if (!projectId || !date) {
@ -44,7 +30,7 @@ async function va_timesheet_summary_function(req, res) {
// Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({
_id: projectId,
"vaAssigned.vaId": vaId
vaAssigned: vaId
});
if (!project) {
@ -163,10 +149,8 @@ async function va_timesheet_summary_function(req, res) {
}
};
const dayNumber = getDayNumber();
// 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 pendingHours = Math.max(0, dedicatedHours - totalWorkedHours);

View File

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

View File

@ -234,7 +234,7 @@ async function removeEntityRelationships(data) {
}}
);
}
// Remove projectId from VAs
if (vaId && Array.isArray(vaId)) {
// await VAModel.updateMany(

View File

@ -4,10 +4,8 @@ import logger from "../config/logger.js";
async function authValidator(req, res, next) {
try {
const sf_token = req.headers["x-auth-token"];
const sf_token = req.headers.sf_token;
if (sf_token) {
const { vaId } = req.params;
if(vaId) req.body.vaId = vaId;
next();
} else {
const accessToken = req.headers.token;
@ -50,15 +48,6 @@ async function authValidator(req, res, next) {
async function clientAuthValidator(req, res, next) {
try {
const sf_token = req.headers["x-auth-token"];
if (sf_token) {
next();
}
if(sf_token) return;
const accessToken = req.headers.token;
if (!accessToken) {

View File

@ -46,9 +46,6 @@ const userDetailsSchema = new Schema({
veemAccount: {
type: String,
},
wiseEmail: {
type: String,
},
phone: {
type: String,
},
@ -89,46 +86,19 @@ const projectSchema = new Schema({
},
vaAssigned: [
{
_id: false,
vaId: {
type: Types.ObjectId,
ref: "VA",
},
isFlexibleAllowed: {
type: Boolean,
default: true
}
}
type: Types.ObjectId,
ref: "VA",
},
],
dedicatedHours: {
type: Array,
default: ["0", "0", "0", "0", "0", "0", "0"]
type: String,
default: "0"
},
status: {
type: String,
enum: ["ACTIVE", "INACTIVE"],
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 ---------------------- */
@ -136,12 +106,6 @@ const clientDetailsSchema = new Schema({
name: {
type: String,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
email: {
type: String,
required: true,
@ -202,7 +166,7 @@ const clientDetailsSchema = new Schema({
enum: ["CHECKING", "SAVINGS"],
type: String,
},
accountHolderName: {
type: String,
},
@ -350,6 +314,12 @@ const vaWorkTrackSchema = new Schema({
type: Types.ObjectId,
ref: "TrackLog",
},
title: {
type: String,
},
appName: {
type: String,
},
from: {
type: Date,
default: Date.now,
@ -362,45 +332,41 @@ const vaWorkTrackSchema = new Schema({
type: Number,
default: 0,
},
logs: {
type: Array,
default: [],
}
},
{ timestamps: true }
)
const passwordResetSchema = new mongoose.Schema({
email: {
type: String,
required: true,
lowercase: true,
index: true
},
token: {
type: String,
required: true,
unique: true,
index: true
},
userType: {
type: String,
required: true,
enum: ['va', 'client'],
index: true
},
expiresAt: {
type: Date,
required: true,
index: true,
expires: 3600 // TTL index for 1 hour
},
used: {
type: Boolean,
default: false
}
email: {
type: String,
required: true,
lowercase: true,
index: true
},
token: {
type: String,
required: true,
unique: true,
index: true
},
userType: {
type: String,
required: true,
enum: ['va', 'client'],
index: true
},
expiresAt: {
type: Date,
required: true,
index: true,
expires: 3600 // TTL index for 1 hour
},
used: {
type: Boolean,
default: false
}
}, {
timestamps: true
timestamps: true
});
passwordResetSchema.index({ email: 1, used: 1 });
@ -435,14 +401,6 @@ const adjustedHoursSchema = new Schema({
type: String,
required: true
},
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
@ -492,14 +450,6 @@ const leavesSchema = new Schema({
type: String,
required: true
},
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
leaveType: {
type: String,
enum: ["SICK", "PTO"],
@ -519,37 +469,6 @@ const leavesSchema = new Schema({
}
}, { 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 ---------------------- */
const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema);
@ -559,6 +478,5 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
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 };

View File

@ -13,7 +13,7 @@ class VaClientRepository {
async findByEmail(email) {
try {
const response = await ClientModel.findOne({ "userDetails.email": email });
const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() });
return response;
} catch (error) {
throw error;
@ -31,27 +31,10 @@ class VaClientRepository {
async update(id, data) {
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(
id,
{ $set: updateObj },
{ new: true, runValidators: true }
{ $set: { ...data } },
{ new: true }
);
return response;
} catch (error) {
@ -59,53 +42,9 @@ class VaClientRepository {
}
}
async findProfileByEmail(email) {
async delete(id) {
try {
const response = await ClientModel.findOne(
{ "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);
const response = await ClientModel.findByIdAndDelete(id);
return response;
} catch (error) {
throw error;

View File

@ -1,7 +1,7 @@
import express from "express";
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_projects_callback } from "../../controllers/va/vaClients/vaClientProject.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_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.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_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";
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js"
// clients routes
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.post("/forgot-password", client_request_password_reset);
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/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
// dashboard API's
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
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("/profile", clientAuthValidator, get_client_dashboard_profile);
clientRoutes.patch("/profile", clientAuthValidator, update_client_password);
clientRoutes.patch("/:clientId", update_client);
clientRoutes.delete("/:clientId", delete_client);

View File

@ -12,15 +12,13 @@ import { process_reset } from "../../controllers/passwordReset.controllers.js";
import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js"
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.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 __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
v1Routes.get("/auth", function (req, res) {
v1Routes.get("/auth", function(req, res) {
res.sendFile(path.join(__dirname, '/auth.html'));
});
@ -31,54 +29,12 @@ v1Routes.use("/client", clientRoutes);
v1Routes.post("/project", authValidator, project_add_sf);
v1Routes.patch("/project/:projectId", authValidator, update_project_sf);
v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
v1Routes.post("/password/reset", process_reset);
v1Routes.get("/", function (req, res) {
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.get("/", function(req, res) {
res.json({"connected": true})
})
// v1Routes.use("/form_submit", authValidator, formRoutes);
export default v1Routes;

View File

@ -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_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.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.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.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
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.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);
// 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/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.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function);
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// 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
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
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/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/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report
// 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;

View File

@ -18,7 +18,6 @@ import apiRoutes from './routes/index.js'
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
import expressLayouts from "express-ejs-layouts";
import jwt from "jsonwebtoken";
const app = express();
@ -27,6 +26,19 @@ const app = express();
const server = new createServer(app);
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) => {
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.use(expressLayouts);
// DEFAULT layout for all pages
app.set("layout", "layouts/dashboard");
const PORT = process.env.PORT || 8000;
app.use(cors({
@ -65,9 +72,7 @@ app.use('/api', apiRoutes);
app.get("/download-tracker", (req, res) => {
res.render("download_tracker", {
title: 'Download tracker',
version: "3.2.1",
layout: 'layouts/main'
version: "3.2.1"
});
});
@ -82,82 +87,53 @@ app.get("/download-file", (req, res) => {
});
app.get('/varegister/:sf_token?', (req, res) => {
const { sf_token } = req.params;
const { sf_token } = req.params;
// No token → invalid request
if (!sf_token) {
return res.status(404).render('not_found', {
title: 'Page Not Found',
layout: 'layouts/error'
});
}
try {
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
try {
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
if (decoded) {
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
return res.render('va_register', {
title: 'VA Registration',
email: decoded.email,
layout: 'layouts/varegisterlayout'
});
} catch (error) {
// Invalid / expired token
return res.status(401).render('not_found', {
title: 'Invalid or Expired Link',
layout: 'layouts/error'
});
}
// if(verified) {
// res.render('va_register', { title: "VA Registration" });
// } else {
// res.render('not_founf', { title: "VA Registration" });
// }
});
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 (!sf_token) {
return res.status(404).render('not_found', {
title: 'Page Not Found',
layout: 'layouts/error'
});
}
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'
});
}
if (decoded) {
res.render('va_client_register', { title: "Client Registration" });
} else {
res.render('not_found', { title: "Client Registration" });
}
} catch (error) {
res.render('not_found', { title: "Client Registration" });
}
});
app.get('/auth/password/reset-form', render_reset_form);
app.get("/client/login", (req, res) => {
// res.render("client_login")
res.render("client/client_login", {
layout: "layouts/auth", // ⬅️ override layout
title: "DocVA | Login"
});
app.get('/health', (req, res) => {
res.status(201).send({ status: "Server is healthy" });
})
app.get("/client/login", (req, res) => {
res.render("client_login")
})
app.get("/client/forgot-password", (req, res) => {
res.render("forgot_password", {
title: "Forgot Password",
layout: "layouts/forgotpasswordlayout"
});
res.render("forgot_password")
})
// Client Dashboard Routes with new file names
@ -165,11 +141,7 @@ app.get('/client/dashboard', async (req, res) => {
try {
// const dashboardData = await dashboardService.getClientDashboard(req.user.id);
// res.render('client/dashboard-dashboard', dashboardData);
// res.render('client/dashboard-dashboard');
res.render('client/dashboard-dashboard', {
title: "Client Dashboard",
pageScript: "dashboard.js"
});
res.render('client/dashboard-dashboard');
} catch (error) {
console.error('Dashboard error:', error);
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) => {
try {
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
// 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');
// res.render('client/dashboard-virtual-assistants', vaData);
} catch (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) => {
try {
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
// res.render('client/dashboard-projects');
res.render('client/dashboard-projects', {
title: "Projects",
pageScript:"projects.js"
});
res.render('client/dashboard-projects');
} catch (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) => {
const { projectId, vaId } = req.params;
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;
@ -277,45 +184,36 @@ app.get("/live/:projectId/:vaId", async (req, res) => {
}
} catch (err) {
console.error("Error fetching client for project:", err.message);
return res.status(500).render("not_found", { title: "Server Error" });
return;
}
if (!clientId) {
return res.status(400).render("not_found", { title: "Page not found" });
if (!clientId) return res.status(400).send({ "message": "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", {
title: "VA Live Tracker",
projectId,
vaId,
clientId,
layout: "layouts/valivetrackerlayout" // ✅ ADD THIS
});
});
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" });
// 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" });
// }
// 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 })
// })
res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
})
// Handling Socket
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
@ -392,8 +290,6 @@ io.use(async (socket, next) => {
if (clientUser) {
socket.data.role = "client";
socket.data.clientId = clientUser._id.toString();
socket.data.vaId = vaId.toString();
socket.data.projectId = projectId;
// Get all VA IDs assigned to this client
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
@ -414,7 +310,7 @@ io.on("connection", (socket) => {
console.log("User connected:", socket.id);
// 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") {
socket.data.role = "va";
socket.data.vaId = id;
@ -423,35 +319,23 @@ io.on("connection", (socket) => {
if (role === "client") {
socket.data.role = "client";
socket.data.clientId = clientId;
socket.data.clientId = id;
// 🧩 Get allowed VAs for this client
const client = await ClientModel.findById(clientId);
const a = JSON.stringify(client);
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 });
}
const client = await ClientModel.findById(id)
.populate("projectIds") // to access their projects
.populate("vaIds"); // optional if you track directly
// Collect all VA IDs from their projects
// const projectVAIds = new Set();
// for (const project of client.projectIds) {
// const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
// populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
// }
const projectVAIds = new Set();
for (const project of client.projectIds) {
const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
}
// // Store allowed VA IDs in socket memory
// socket.data.allowedVAs = Array.from(projectVAIds);
// console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
// Store allowed VA IDs in socket memory
socket.data.allowedVAs = Array.from(projectVAIds);
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.on("requestVisibleApps", async ({ token }) => {
socket.on("getVisibleApps", async ({ token, apps }) => {
try {
// Verify 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();
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
for (let [_, clientSocket] of io.sockets.sockets) {
if (
@ -567,77 +422,15 @@ io.on("connection", (socket) => {
});
// socket.on('disconnect', () => {
// 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 });
}
}
socket.on('disconnect', () => {
console.log('a user Disconnected');
});
});
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) => {
res.status(404).render("not_found", {
layout: "layouts/error",
title: "404 - Page Not Found"
});
res.status(404).render("not_found", {
title: "404 - Page Not Found"
});
});
server.listen(PORT, () => {

View File

@ -2,27 +2,25 @@ import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import { VAModel, ClientModel } from "../models/vaModel.js";
import logger from "../config/logger.js";
import axios from "axios";
import authFetch from "../utils/authFetch.js";
class PasswordResetService {
constructor(passwordResetRepository) {
constructor(passwordResetRepository, emailService) {
this.passwordResetRepository = passwordResetRepository;
// this.emailService = emailService;
this.emailService = emailService;
}
async requestPasswordReset(email, userType) {
try {
// Determine which model to use
const userModel = userType === 'va' ? VAModel : ClientModel;
// Check if user exists
const user = await userModel.findOne({ "userDetails.email": email.toLowerCase() });
if (!user) {
logger.info(`Password reset requested for non-existent ${userType}: ${email}`);
return {
success: false,
message: "Email not recognized. Please sign up to continue."
return {
success: false,
message: "Email not recognized. Please sign up to continue."
};
}
@ -31,7 +29,7 @@ class PasswordResetService {
// Generate JWT token
const token = jwt.sign(
{
{
email: email.toLowerCase(),
userType: userType,
type: 'password_reset'
@ -62,9 +60,9 @@ class PasswordResetService {
await this.sendResetEmail(email, resetLink, userType);
logger.info(`Password reset token created for ${userType}: ${email}`);
return {
success: true,
message: "Email sent successfully. Please check your inbox."
return {
success: true,
message: "If the email exists, a reset link has been sent"
};
} catch (error) {
@ -94,10 +92,10 @@ class PasswordResetService {
return { valid: false, message: "Invalid or expired token" };
}
return {
valid: true,
return {
valid: true,
email: decoded.email,
userType: decoded.userType
userType: decoded.userType
};
} catch (error) {
@ -131,7 +129,7 @@ class PasswordResetService {
const userModel = userType === 'va' ? VAModel : ClientModel;
// Check if user exists and has matching token
const user = await userModel.findOne({
const user = await userModel.findOne({
"userDetails.email": decoded.email,
passwordResetToken: token
});
@ -152,12 +150,12 @@ class PasswordResetService {
await resetRecord.save();
// Send confirmation email
// await this.sendPasswordChangedEmail(decoded.email, userType);
await this.sendPasswordChangedEmail(decoded.email, userType);
logger.info(`Password successfully reset for ${userType}: ${decoded.email}`);
return {
success: true,
message: "Password has been reset successfully"
return {
success: true,
message: "Password has been reset successfully"
};
} catch (error) {
@ -168,21 +166,17 @@ class PasswordResetService {
async sendResetEmail(email, resetLink, userType) {
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) {
logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`);
throw new Error("Failed to send reset email");
@ -192,7 +186,7 @@ class PasswordResetService {
async sendPasswordChangedEmail(email, userType) {
try {
const userTypeText = userType === 'va' ? 'Virtual Assistant' : 'Client';
const emailData = {
to: email,
subject: `${userTypeText} - Password Changed Successfully`,

View File

@ -1,31 +1,29 @@
import axios from "axios";
import logger from "../config/logger.js";
import authFetch from "../utils/authFetch.js";
class SalesforceService {
async registerClientInSalesforce(clientData, _id) {
try {
const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`;
// Extract only the required data for Salesforce
const salesforcePayload = this.prepareSalesforcePayload(clientData, _id);
logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`);
const response = await authFetch(url, {
method: 'POST',
const response = await axios.post(url, salesforcePayload, {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(salesforcePayload),
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;
} catch (error) {
logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`);
if (error.response) {
// Salesforce returned an error response
logger.error(`SalesforceService - Salesforce API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
@ -43,13 +41,11 @@ class SalesforceService {
prepareSalesforcePayload(clientData, _id) {
const { userDetails } = clientData;
return {
userDetails: {
_id: _id,
name: userDetails.name,
firstName: userDetails.firstName,
lastName: userDetails.lastName,
email: userDetails.email.toLowerCase(),
address: userDetails.address || null,
city: userDetails.city || null,

View File

@ -1,7 +1,5 @@
import logger from "../config/logger.js";
import SalesforceService from "./salesforce.service.js";
import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
import bcrypt from "bcrypt";
class VaClientService {
constructor(vaClientRepository) {
@ -18,7 +16,7 @@ class VaClientService {
}
// Sanitize and prepare data
const clientData = await this.sanitizeClientData(data);
const clientData = this.sanitizeClientData(data);
const response = await this.clientRepository.create(clientData);
if(response) {
@ -55,227 +53,34 @@ class VaClientService {
}
async updateClient(id, data) {
try {
// Check if client exists
const existingClient = await this.clientRepository.findById(id);
if (!existingClient) {
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
}
};
try {
const response = await this.clientRepository.update(id, data);
if (!response) throw new Error("Client not found");
return response;
} catch (error) {
logger.error("Client Service - updateClient error: " + error.message);
throw error;
}
}
async deleteClient(clientId) {
async deleteClient(id) {
try {
// Check if client exists
const existingClient = await this.clientRepository.findById(clientId);
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
};
const response = await this.clientRepository.delete(id);
return response;
} catch (error) {
logger.error("Client Service - deleteClient error: " + error.message);
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
async sanitizeClientData(payload) {
sanitizeClientData(payload) {
const { userDetails } = payload;
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
return {
userDetails: {
firstName: userDetails.firstName,
lastName: userDetails.lastName,
name: userDetails.name,
email: userDetails.email.toLowerCase(),
password: hashedPassword,
address: userDetails.address || null,
city: userDetails.city || null,
state: userDetails.state || null,

View File

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

View File

@ -1,5 +0,0 @@
const convertToObjectId = (id) => {
return mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : id;
}
export default convertToObjectId;

View File

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

View File

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

View File

@ -18,7 +18,7 @@
if (userType === 'client') {
forgotPasswordUrl = '/client/forgot-password';
loginUrl = '/';
loginUrl = '/client/login';
}
%>
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>

View File

@ -36,7 +36,7 @@
<%
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
if (userType === 'client') {
backToLoginUrl = '/';
backToLoginUrl = '/client/login';
}
%>
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>

View File

@ -15,7 +15,7 @@
<%
let loginUrl = `${process.env.FRONTEND_URL}/login`;
if (userType === 'client') {
loginUrl = '/';
loginUrl = '/client/login';
}
%>
<a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a>

View File

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

View File

@ -1,76 +1,371 @@
<!-- 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>
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocVA Client | Dashboard</title>
<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 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>
</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 active">
<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">
<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>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>
<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>
<script>
const tnyk = "<%= typeof sf_token !== 'undefined' ? sf_token : '' %>";
if (tnyk) {
localStorage.setItem('knoptpq', tnyk);
}
</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;
}
// 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>

View File

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

View File

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

View File

@ -1,31 +1,378 @@
<!-- 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>
</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>
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocVA Client | Dashboard</title>
<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">
<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 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>
</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>
<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>

View File

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

View File

@ -1,69 +1,371 @@
<!-- 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>
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocVA Client | Dashboard</title>
<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">
<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>
</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 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" style="justify-content: space-between;align-items: center;">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
<a href="/client/virtual-assistants/manage-tips" class="btn btn-primary">Manage Tips</a>
</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;">
<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;
}
// 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-users"></i>
<h3>No Virtual Assistants</h3>
<p>You haven't assigned any virtual assistants yet.</p>
<i class="fas fa-exclamation-triangle"></i>
<h3>Error</h3>
<p>${message}</p>
</div>
</div>
</div>
</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>

View File

@ -213,7 +213,7 @@
<body>
<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 class="login-card">
@ -224,7 +224,7 @@
<input type="password" name="password" placeholder="Enter your password *" required />
<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>
@ -236,10 +236,6 @@
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);
}
@ -280,14 +276,14 @@
if (!res.ok) {
throw new Error(data.message || "Login failed");
}
if (data.success) {
if(data.userType === "CLIENT") {
localStorage.setItem("knoptpq", data.accessToken);
window.location.href = "/client/dashboard";
} else {
window.location.href = "/download-tracker";
}
// 🟢 Save token
localStorage.setItem("knoptpq", data.accessToken);
// localStorage.setItem("refreshToken", data.refreshToken);
// 🟢 Redirect
window.location.href = "/client/dashboard";
} else {
alert("No token received. Please try again.");
}

View File

@ -139,7 +139,7 @@
<script>
const osButtons = document.querySelectorAll(".os-btn");
const downloadBtn = document.getElementById("downloadBtn");
const version = "<%= version || '1.1.1' %>";
const version = "<%= version || '3.2.1' %>";
let selectedOS = "windows";
@ -171,21 +171,7 @@
// -------- Download --------
downloadBtn.addEventListener("click", () => {
const activeBtn = document.querySelector(".os-btn.active")
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 = ``;
window.location.href = `/download-file?os=${selectedOS}`;
});
</script>

View File

@ -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="logo">
<img id="logo-img" src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
</div>
<h2>DocTracker</h2>
<form class="card">
<h2>Reset your password</h2>
<input type="email" placeholder="Enter your email address*" required />
@ -12,24 +127,6 @@
</div>
<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) {
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);
@ -103,3 +200,6 @@
});
});
</script>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,5 @@
</head>
<body>
<%- body %>
<script src="/public/js/common/theme.js"></script>
</body>
</html>

View File

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

View File

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

View File

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

View File

@ -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">
<img src="/public/images/logo-white-vertical.svg" alt="404 Icon">
<h1>404</h1>
<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>
@ -29,3 +124,6 @@
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme);
</script>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 -->
<div class="sidebar">
<div class="logo">
@ -9,7 +21,7 @@
<nav class="progress-steps">
<div class="step active" data-step="1" onclick="goToStep(1)">
<div class="step-number">1</div>
<!-- <div class="step-number">✓</div> -->
<!-- <div class="step-number">✓</div> -->
<div class="step-text">Create Your Account</div>
<div class="step-connector"></div>
</div>
@ -47,7 +59,7 @@
<ol>
<li class="step active" data-step="1" onclick="goToStep(1)">
<div class="step-number">1</div>
<!-- <div class="step-number">✓</div> -->
<!-- <div class="step-number">✓</div> -->
<div class="step-text">Create Account</div>
</li>
<li class="step" data-step="2" onclick="goToStep(2)">
@ -86,47 +98,19 @@
<form id="account-form">
<div class="form-group">
<div class="input-container">
<input type="text" id="first-name" class="input-field" placeholder=" " oninput="validateFirstName(this)" required>
<label for="first-name" class="input-label">First Name *</label>
<input type="text" id="company-name" class="input-field" placeholder=" " oninput="validateCompanyName(this)" required>
<label for="company-name" class="input-label">Company / Client Name *</label>
</div>
<div class="invalid-feedback" id="first-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
<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="input-container">
<input type="text" id="last-name" class="input-field" placeholder=" " oninput="validateLastName(this)" 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>
<input type="email" id="email" class="input-field" placeholder=" " required>
<label for="email" class="input-label">Email *</label>
</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></div> <!-- Empty div for spacing -->
<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>
<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="input-container">
<input type="text" id="address" class="input-field" placeholder=" " required>
@ -158,21 +133,18 @@
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="city" class="input-field" placeholder=" "
oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)"
required>
<input type="text" id="city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
<label for="city" class="input-label">City *</label>
</div>
<div class="invalid-feedback" id="city-error"
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
<div class="invalid-feedback" id="city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
City 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="state" class="input-field" placeholder=" " required> -->
<select id="state" class="select-field" required>
<option value="" disable selected style="display: none"></option>
<select id="state" class="select-field" required>
<option value=""></option>
<!-- States will be populated by JavaScript -->
</select>
<label for="state" class="input-label">State *</label>
@ -181,14 +153,11 @@
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="zip" class="input-field" placeholder=" "
oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)"
maxlength="10" required>
<input type="text" id="zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="zip" class="input-label">ZIP *</label>
</div>
<div class="invalid-feedback" id="zip-error"
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Invalid Zip code
<div class="invalid-feedback" id="zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div>
<div class="form-actions">
@ -223,21 +192,18 @@
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="billing-city" class="input-field" placeholder=" "
oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)"
required>
<input type="text" id="billing-city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
<label for="billing-city" class="input-label">City *</label>
</div>
<div class="invalid-feedback" id="billing-city-error"
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
<div class="invalid-feedback" id="billing-city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
City 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="billing-state" class="input-field" placeholder=" " required> -->
<select id="billing-state" class="select-field" required>
<option value="" disable selected style="display: none"></option>
<select id="billing-state" class="select-field" required>
<option value=""></option>
<!-- States will be populated by JavaScript -->
</select>
<label for="billing-state" class="input-label">State *</label>
@ -245,13 +211,10 @@
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="billing-zip" class="input-field" placeholder=" "
oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)"
maxlength="10" required>
<input type="text" id="billing-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="billing-zip" class="input-label">ZIP *</label>
</div>
<div class="invalid-feedback" id="billing-zip-error"
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
<div class="invalid-feedback" id="billing-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div>
@ -279,31 +242,24 @@
<div class="error-message" id="step-4-error-message"></div>
</div>
<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>
<form id="signatory-form">
<div class="form-group">
<div class="input-container">
<input type="text" id="position" class="input-field" placeholder=" "
oninput="validatePosition(this)"
onkeypress="return preventInvalidPositionChars(event)" required>
<label for="position" class="input-label">Position (Owner / CEO / Manager / etc.)
*</label>
<input type="text" id="position" class="input-field" placeholder=" " oninput="validatePosition(this)" onkeypress="return preventInvalidPositionChars(event)" required>
<label for="position" class="input-label">Position (Owner / CEO / Manager / etc.) *</label>
</div>
<div class="invalid-feedback" id="position-error"
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
<div class="invalid-feedback" id="position-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Position should only contain letters, spaces, and common punctuation
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="signatory-name" class="input-field" placeholder=" "
oninput="validateSignatoryName(this)" onkeypress="return preventNumbers(event)"
required>
<input type="text" id="signatory-name" class="input-field" placeholder=" " oninput="validateSignatoryName(this)" onkeypress="return preventNumbers(event)" required>
<label for="signatory-name" class="input-label">Name *</label>
</div>
<div class="invalid-feedback" id="signatory-name-error"
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
<div class="invalid-feedback" id="signatory-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Name should not contain numbers
</div>
</div>
@ -341,63 +297,50 @@
<div class="row">
<div class="form-group">
<div class="input-container">
<input type="text" id="expiry-date" class="input-field" placeholder=" "
required>
<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)"
nkeypress="return preventInvalidCVVChars(event)" maxlength="4" required>
<input type="text" id="security-code" class="input-field" placeholder=" " oninput="validateCVVVV(this)" nkeypress="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;">
<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>
<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;">
<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>
<h4>Bank Account (ACR)</h4>
<p>Direct bank transfer</p>
</div>
<div id="bank-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="bank-name" class="input-field" placeholder=" "
oninput="validateBankName(this)"
onkeypress="return preventInvalidBankNameChars(event)" required>
<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;">
<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>
<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;">
<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>
@ -410,7 +353,7 @@
<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=""></option>
<option value="checking">Checking</option>
<option value="savings">Savings</option>
</select>
@ -419,13 +362,10 @@
</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>
<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;">
<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>
@ -442,26 +382,13 @@
<div class="success-message">
<div class="success-icon">✓</div>
<h3>Registration Complete!</h3>
<p>Your account has been successfully created. You will receive a confirmation email shortly.
</p>
<p>Your account has been successfully created. You will receive a confirmation email shortly.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/public/js/va-client-register.js"></script>
<script>
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 = getSystemTheme();
setTheme(savedTheme);
</script>
<script src="/public/js/va-client-register.js"></script>
</body>
</html>

View File

@ -1,124 +1,287 @@
<!-- Mobile Toggle Button -->
<div class="mobile-toggle" id="toggleBtn">
<span></span>
<span></span>
<span></span>
</div>
<!DOCTYPE html>
<html lang="en">
<!-- Mobile Overlay -->
<div id="overlay"></div>
<head>
<meta charset="UTF-8" />
<title>Live Screen Feed</title>
<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.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;
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
apps.forEach(app => {
const div = document.createElement("div");
div.classList.add("app");
div.textContent = app.name || "Unnamed App";
body {
height: 100vh;
display: flex;
background: #0f0f0f;
color: white;
font-family: "Inter", sans-serif;
overflow: hidden;
}
div.onclick = () => {
document.querySelectorAll(".app").forEach(el => el.classList.remove("active"));
div.classList.add("active");
/* SIDEBAR */
.sidebar {
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;
selectedAppTitle.textContent = app.name;
.sidebar h3 {
margin-bottom: 15px;
font-size: 18px;
font-weight: 600;
}
placeholder.style.display = "none";
liveImage.style.display = "none";
.app {
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
sidebar.classList.remove("open");
overlay.classList.remove("show");
};
.app.active {
background: #6c63ff;
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.disconnect();
localStorage.removeItem("knoptpq");
window.location.href = "/client/login";
});
socket.emit("register", {
role: "client",
id: vaId,
projectId: "<%= projectId %>",
clientId: clientId
});
/* Receive images */
socket.on("receiveImage", (base64, title) => {
if (currentApp && title === currentApp) {
liveImage.src = base64;
liveImage.style.display = "block";
placeholder.style.display = "none";
}
});
</script>
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");
};
/* 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>

View File

@ -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">
<!-- Sidebar for desktop -->
@ -77,28 +171,28 @@
<form id="account-form">
<div class="form-group">
<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>
</div>
</div>
<div class="form-group">
<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>
</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-group">
<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>
</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">
@ -165,32 +259,21 @@
</div>
<div class="form-group">
<div class="input-container">
<select id="country" class="select-field" required onchange="handleChangeSelect(this)">
<option value="" disabled selected></option>
<select id="country" class="select-field" required>
<option value=""></option>
</select>
<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 class="form-group">
<div class="input-container">
<input type="text" id="state" class="input-field" placeholder=" " required>
<!-- <select id="state" class="select-field" required onchange="handleChangeSelect(this)">
<select id="state" class="select-field" required>
<option value=""></option>
</select> -->
<label for="state" class="input-label">State / Region *</label>
<!-- <div class="location-loading" id="state-loading">Loading states...</div> -->
</div>
</div>
<div class="form-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>
<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>
@ -212,19 +295,19 @@
<label for="phone" class="input-label">Phone Number *</label>
<div class="input-container">
<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 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)
</div>
</div>
<!-- <div class="form-group">
<div class="form-group">
<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>
</div>
</div> -->
</div>
<div class="form-actions">
<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>
<form id="payment-form">
<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="form-group">
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
@ -281,11 +331,27 @@
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="email" id="payoneerAccount" class="input-field" placeholder=" " oninput="validatePayoneerEmail(this)" required>
<label for="payoneerAccount" class="input-label">Payoneer Email Address *</label>
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
</div>
<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>
@ -311,18 +377,5 @@
</div>
</div>
<script src="/public/js/va-register.js"></script>
<script>
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 = getSystemTheme();
setTheme(savedTheme);
</script>
</body>
</html>