Compare commits

..

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

114 changed files with 1382 additions and 16225 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 {
@ -63,13 +20,12 @@ body {
.sidebar {
display: none;
flex-direction: column;
width: 388px;
background: var(--bg-sidebar);
width: 280px;
background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0);
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 {
@ -754,37 +475,4 @@ body {
font-size: 40px;
margin-bottom: 40px;
}
}
@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);
}
}
@media print {
[data-theme="dark"] {
--bg-primary: #ffffff;
--bg-card: #ffffff;
--text-primary: #000000;
--text-secondary: #333333;
--border-color: #cccccc;
}
.sidebar {
display: none;
}
.btn {
display: none;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 B

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

File diff suppressed because it is too large Load Diff

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,53 +80,36 @@
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
document.querySelectorAll('.input-field:not(.dob)').forEach(input => {
setTimeout(() => {
if (input.value) {
input.nextElementSibling.style.top = '4px';
input.nextElementSibling.style.fontSize = '12px';
input.nextElementSibling.style.transform = 'none';
}
}, 100);
input.addEventListener('animationstart', function() {
if (this.value) {
this.nextElementSibling.style.top = '4px';
this.nextElementSibling.style.fontSize = '12px';
this.nextElementSibling.style.transform = 'none';
}
});
document.querySelectorAll('.input-field').forEach(input => {
input.addEventListener('input', function() {
if (this.value) {
this.nextElementSibling.style.top = '4px';
@ -204,13 +139,6 @@
// Handle select field label animation
document.querySelectorAll('.select-field').forEach(select => {
setTimeout(() => {
if (select.value) {
select.nextElementSibling.style.top = '4px';
select.nextElementSibling.style.fontSize = '12px';
select.nextElementSibling.style.transform = 'none';
}
}, 100);
select.addEventListener('change', function() {
if (this.value) {
this.nextElementSibling.style.top = '4px';
@ -259,102 +187,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
@ -396,14 +228,10 @@
return maxStep;
}
async function handleNextStep(step) {
await nextStep(step);
}
// Function to navigate to next step
async function nextStep(step) {
function nextStep(step) {
// Validate current step before proceeding
if (!(await validateStep(currentStep))) {
if (!validateStep(currentStep)) {
return;
}
@ -478,13 +306,9 @@
}
// Function to validate current step
async function validateStep(step) {
function validateStep(step) {
let isValid = true;
const form = document.getElementById(getFormId(step));
// Hide any previous errors for this step
hideError(step);
clearFieldErrors();
if (form) {
// For account setup step
@ -492,42 +316,24 @@
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)) {
showError('Please enter a valid email address', 'email', step);
alert('Please enter a valid email address');
isValid = false;
} else {
// Email verification - wait for the result
const isEmailVerified = await verifyEmail(email);
if (!isEmailVerified) {
showError('Email already registered. Please use a different email address.', 'email', step);
isValid = false;
}
}
// Password validation
if (password.length < 8) {
showError('Password must be at least 8 characters long', 'password', step);
alert('Password must be at least 8 characters long');
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');
}
}
@ -536,18 +342,15 @@
const firstName = document.getElementById('firstName').value;
if (!firstName) {
document.getElementById('profile-incomplete').style.display = 'block';
document.getElementById('firstName').classList.add('field-error');
showError('First name is required', 'firstName', step);
isValid = false;
} else {
document.getElementById('profile-incomplete').style.display = 'none';
document.getElementById('firstName').classList.remove('field-error');
}
// 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);
alert('You must be 18 years or older to register');
isValid = false;
}
@ -556,12 +359,12 @@
const state = document.getElementById('state').value;
if (!country) {
showError('Please select your country', 'country', step);
alert('Please select your country');
isValid = false;
}
if (!state) {
showError('Please select your state/region', 'state', step);
alert('Please select your state/region');
isValid = false;
}
@ -570,40 +373,9 @@
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.classList.add('field-error');
if (isValid === false) {
showError(`Please fill in ${field.labels[0]?.textContent || 'this field'}`, field.id, step);
}
field.style.border = '1px solid #ef4444';
} else {
field.classList.remove('field-error');
}
});
}
if (step === 3) {
// Phone number validation
const phone = document.getElementById('phone').value;
if (!phone) {
showError('Please enter your phone number', 'phone', step);
isValid = false;
} else if (!validatePhoneNumberStrict(phone)) {
document.getElementById('phone-error').style.display = 'block';
document.getElementById('phone').classList.add('field-error');
showError('Please enter a valid phone number (10-15 digits)', 'phone', step);
isValid = false;
} else {
document.getElementById('phone-error').style.display = 'none';
document.getElementById('phone').classList.remove('field-error');
}
// Validate other required fields in step 3
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.classList.add('field-error');
} else {
field.classList.remove('field-error');
field.style.border = '';
}
});
}
@ -612,38 +384,23 @@
if (step === 4) {
const selectedPayment = document.querySelector('.payment-option.selected');
if (!selectedPayment) {
showError('Please select a payment method', null, step);
alert('Please select a payment method');
isValid = false;
} else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) {
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) {
alert('Please enter your Payoneer account number');
isValid = false;
}
} else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) {
alert('Please enter your Veem account number');
isValid = false;
}
}
}
}
@ -653,9 +410,9 @@
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.classList.add('field-error');
field.style.border = '1px solid #ef4444';
} else {
field.classList.remove('field-error');
field.style.border = '';
}
});
}
@ -697,11 +454,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 +465,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 +495,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;
@ -906,17 +593,14 @@
});
nextStep(5);
setTimeout(() => {
window.location.href = '/download-tracker';
}, 1000);
} else {
// Handle server errors
showError('There was an error submitting your registration. Please try again.', null, 4);
alert('There was an error submitting your registration. Please try again.');
}
} catch (error) {
// Handle network errors
console.error('Error:', error);
showError('There was a network error. Please check your connection and try again.', null, 4);
alert('There was a network error. Please check your connection and try again.');
} finally {
// Restore button state
submitBtn.textContent = originalText;
@ -924,285 +608,3 @@
}
}
async function verifyEmail(email) {
try {
const response = await fetch('/api/v1/va/verify-va-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"email": email})
});
if (response.ok) {
return false;
} else {
return true;
}
} catch (error) {
// Handle network errors
console.error('Error:', error);
showError('There was a network error. Please check your connection and try again.', null, 1);
return false;
}
}
// Function to calculate age from date of birth
function calculateAge(dob) {
if (!dob) return '';
const birthDate = new Date(dob);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
// Adjust age if birthday hasn't occurred this year yet
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
// Function to handle date of birth change and update age
function handleDOBChange() {
const dob = document.getElementById('dob').value;
const ageInput = document.getElementById('age');
if (dob) {
const age = calculateAge(dob);
ageInput.value = age;
// Trigger input event to update the label position
ageInput.dispatchEvent(new Event('input'));
// 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');
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');
ageInput.style.border = '1px solid #ef4444';
} else {
ageInput.style.border = '';
}
} else {
// Clear age if DOB is cleared
ageInput.value = '';
ageInput.dispatchEvent(new Event('input'));
}
}
// Function to handle age manual input and update DOB (optional)
function handleAgeChange() {
const age = document.getElementById('age').value;
const dobInput = document.getElementById('dob');
if (age && age >= 18 && age <= 100) {
// Calculate approximate DOB based on age
const today = new Date();
const birthYear = today.getFullYear() - age;
const approximateDOB = `${birthYear}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
// You can set this as the DOB or just leave it for reference
// dobInput.value = approximateDOB;
// dobInput.dispatchEvent(new Event('change'));
}
}
// Function to validate phone number in real-time
function validatePhoneNumber(input) {
const phoneError = document.getElementById('phone-error');
const value = input.value;
// Remove any non-digit characters
const digitsOnly = value.replace(/\D/g, '');
// Update the input value with digits only
if (value !== digitsOnly) {
input.value = digitsOnly;
}
// Validate length
if (digitsOnly.length > 0 && (digitsOnly.length < 10 || digitsOnly.length > 15)) {
phoneError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
} else {
phoneError.style.display = 'none';
input.style.border = '';
return true;
}
}
// Function to validate phone number on form submission
function validatePhoneNumberStrict(phone) {
const phoneRegex = /^[0-9]{10,15}$/;
return phoneRegex.test(phone);
}
// Function to validate Payoneer account number
function validatePayoneerAccount(input) {
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;
}
}
return true;
}
// Function to validate Veem account number
function validateVeemAccount(input) {
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;
}
}
return true;
}
function showError(message, fieldId = null, step = null) {
// Determine the current step if not provided
const currentStep = step || getCurrentStep();
const errorContainer = document.getElementById(`step-${currentStep}-error`);
const errorMessage = document.getElementById(`step-${currentStep}-error-message`);
if (errorContainer && errorMessage) {
errorMessage.textContent = message;
errorContainer.style.display = 'block';
// Scroll to error container
errorContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Highlight specific field if provided
if (fieldId) {
const field = document.getElementById(fieldId);
if (field) {
field.classList.add('field-error');
// Remove error after 5 seconds
setTimeout(() => {
field.classList.remove('field-error');
}, 5000);
}
}
// Auto-hide error after 8 seconds
setTimeout(() => {
hideError(currentStep);
}, 8000);
}
}
function hideError(step = null) {
if (step) {
// Hide specific step error
const errorContainer = document.getElementById(`step-${step}-error`);
if (errorContainer) {
errorContainer.style.display = 'none';
}
} else {
// Hide all step errors
for (let i = 1; i <= 5; i++) {
const errorContainer = document.getElementById(`step-${i}-error`);
if (errorContainer) {
errorContainer.style.display = 'none';
}
}
}
}
function clearFieldErrors() {
document.querySelectorAll('.field-error').forEach(field => {
field.classList.remove('field-error');
});
}
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,367 +0,0 @@
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) {
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" });
}
const clientID = clientUser._id;
// Get all VA IDs and Project IDs assigned to this client
const clientVaIds = clientUser.vaIds || [];
const clientProjectIds = clientUser.projectIds || [];
// Get all VAs assigned to this client (with full details)
const allAssignedVAs = await VAModel.find({
_id: { $in: clientVaIds }
})
.select('userDetails.firstName userDetails.lastName userDetails.email userDetails.phone status createdAt clientIds projectIds')
.lean()
.exec();
// Get All Projects of client
const clientProjects = await ProjectModel.find({
_id: { $in: clientProjectIds }
})
.select('name hourlyRate totalLoggedHours vaAssigned dedicatedHours createdAt clientId')
.lean()
.exec();
// Create project map for easy lookup
const projectMap = clientProjects.reduce((acc, project) => {
acc[project._id.toString()] = project;
return acc;
}, {});
// Find the latest log entry for each VA that belongs to this client and their projects
const agg = await LoggedModel.aggregate([
{
$match: {
vaId: { $in: clientVaIds },
projectId: { $in: clientProjectIds }
}
},
{ $sort: { time: -1 } },
{
$group: {
_id: "$vaId",
lastLog: { $first: "$$ROOT" },
totalLogins: {
$sum: {
$cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0]
}
},
lastActivity: { $first: "$time" }
}
},
{
$match: {
"lastLog.loggedType": "LOGIN"
}
}
]);
const activeVaIds = agg.map((a) => a._id).filter(Boolean);
// 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
};
return acc;
}, {});
// Calculate total worked time for each VA
const totalWorkedTimeByVa = {};
// Get all logs for client's VAs to calculate total worked time
const allLogs = await LoggedModel.find({
vaId: { $in: clientVaIds },
projectId: { $in: clientProjectIds }
})
.sort({ time: 1 })
.lean();
// Group logs by VA
const logsByVa = {};
allLogs.forEach(log => {
const vaId = log.vaId.toString();
if (!logsByVa[vaId]) {
logsByVa[vaId] = [];
}
logsByVa[vaId].push(log);
});
// Calculate total worked time for each VA
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];
if (!logsByDate[date]) {
logsByDate[date] = [];
}
logsByDate[date].push(log);
});
// Process each day separately
for (const [date, dayLogs] of Object.entries(logsByDate)) {
// Check if this day has any adjustment logs
const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
let dayWorkSeconds = 0;
let lastLogin = null;
// If day has adjustment logs, only process adjustment logs
// If no adjustment logs, process all logs for that day
const logsToProcess = hasAdjustmentLogs
? dayLogs.filter(log => log.isAdjustment === true)
: dayLogs;
// Sort logs by time for proper pairing
logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
// Calculate work duration for this day
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;
}
}
totalWorkSeconds += dayWorkSeconds;
}
totalWorkedTimeByVa[vaId] = {
totalSeconds: totalWorkSeconds,
formatted: formatTime(totalWorkSeconds)
};
}
// 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
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 currentProjectDetails = null;
if (lastLog && lastLog.loggedType === "LOGIN") {
const loginTime = new Date(lastLog.time);
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
isCurrentlyActive = true;
// Consider VA inactive if last login was more than 24 hours ago
const hoursSinceLastActivity = (currentTime - loginTime) / (1000 * 60 * 60);
if (hoursSinceLastActivity > 24) {
isCurrentlyActive = false;
}
// 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';
// 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,
email: v.userDetails?.email,
phone: v.userDetails?.phone,
status: v.status || 'active',
isCurrentlyActive: isCurrentlyActive,
lastLoginTime: lastLog?.time || null,
currentSessionDuration: currentSessionDuration,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
lastProject: currentProjectDetails,
lastProjectId: lastLog?.projectId || null,
projectsAssigned: clientProjectsAssigned.length,
// Detailed project information for popup
projects: projectsWithWorkTime,
vaSince: v.createdAt
};
});
const dayNumber = getDayNumber();
// 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 currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive);
const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive);
// Calculate total worked time across all VAs
const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0);
return res.status(200).json({
success: true,
data: {
// Only send data that frontend actually uses
allVAs: allVAsFormatted
},
summary: {
totalVAs: allAssignedVAs.length,
activeVAs: currentlyActiveVAs.length,
inactiveVAs: inactiveVAs.length,
totalProjects: clientProjects.length,
totalHoursLogged: totalHoursLogged,
totalDedicatedHours: totalDedicatedHours,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
utilizationRate: totalDedicatedHours > 0 ?
((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%'
}
});
} catch (error) {
console.error("Client dashboard error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
// Helper function to format time
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
}
export { client_dashboard };

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,176 +0,0 @@
import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken";
import getDayNumber from "../../../utils/getDayNumber.js";
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) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
// Getting client dashboard data - projects with assigned VA details
async function client_dashboard_projects(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;
// 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;
// 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
const allAssignedVAs = await VAModel.find({
_id: { $in: allProjectVaIds.vaId }
})
.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;
}, {});
// 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
};
}));
// 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;
return res.status(200).json({
success: true,
data: {
projects: projectsFormatted
},
counts: {
totalProjects: totalProjects,
totalVAs: allVAsAcrossProjects.size,
activeVAs: totalActiveVAs.size,
projectsWithActiveVAs: projectsWithActiveVAs
}
});
} catch (error) {
console.error("Client dashboard projects error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { client_dashboard_projects };

View File

@ -1,166 +0,0 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel, ClientModel } from "../../../models/vaModel.js";
// Helper function to parse date string in DD-MM-YYYY format
function parseDate(dateString) {
if (!dateString) return null;
const parts = dateString.split('-');
if (parts.length !== 3) return null;
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // Months are 0-indexed in JavaScript
const year = parseInt(parts[2], 10);
// Validate date components
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
if (day < 1 || day > 31 || month < 0 || month > 11 || year < 1900) return null;
return new Date(year, month, day);
}
// Getting logged hours in a specific project of VA
async function client_dashboard_va_log(req, res) {
console.log(req)
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" });
}
const clientID = clientUser._id;
const clientProjectIds = clientUser.projectIds || [];
const payload = req.body || {};
const { vaId } = payload;
// const va = await VAModel.findOne({
// _id: vaId,
// clientIds: clientID
// });
// if (!va) {
// return res.status(403).json({
// success: false,
// message: "VA not found or not assigned to this client"
// });
// }
let logs = await LoggedModel.find({
vaId,
projectId: { $in: clientProjectIds },
})
.sort({ time: 1 })
.populate("projectId", "name") // attach project name
.lean();
if (!logs.length) {
return res.status(200).json({
success: true,
message: "No logs found in given range",
data: [],
totalWorkTime: "00:00:00",
});
}
let totalWorkSeconds = 0;
const projectWork = {}; // key: projectId -> seconds
// Group logs by date (YYYY-MM-DD format)
const logsByDate = {};
logs.forEach(log => {
const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD
if (!logsByDate[date]) {
logsByDate[date] = [];
}
logsByDate[date].push(log);
});
// Process each day separately
for (const [date, dayLogs] of Object.entries(logsByDate)) {
// Check if this day has any adjustment logs
const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
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
const logsToProcess = hasAdjustmentLogs
? dayLogs.filter(log => log.isAdjustment === true)
: dayLogs;
// Sort logs by time for proper pairing (already sorted by query, but ensure)
logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
// Calculate work duration for this day
for (const log of logsToProcess) {
const time = new Date(log.time);
if (log.loggedType === "LOGIN") {
lastLogin = time;
lastProjectId = log.projectId?._id?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
if (lastProjectId) {
projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec;
}
// reset after each pair
lastLogin = null;
lastProjectId = null;
}
}
totalWorkSeconds += dayWorkSeconds;
}
// 🕓 Convert total seconds to hh:mm:ss
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
};
// 🧠 Build project-wise array
const projectDetails = await Promise.all(
Object.entries(projectWork).map(async ([projectId, sec]) => {
const project = await ProjectModel.findById(projectId).lean();
return {
projectId,
projectName: project?.name || "Unknown Project",
totalTime: formatTime(sec),
};
})
);
return res.status(200).json({
success: true,
totalWorkTime: formatTime(totalWorkSeconds),
projects: projectDetails,
});
} catch (error) {
console.log(error)
return res.status(500).send("Internal Server Error");
}
}
export { client_dashboard_va_log };

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,76 @@
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(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,32 +0,0 @@
import { ClientModel } from "../../models/vaModel.js";
// Verify VA Email
async function client_verify_email(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { email } = payload;
if (!email) {
return res.status(400).json({ success: false, message: "Please provide email" });
}
// Find user by email
const user = await ClientModel.findOne({ "userDetails.email": email });
if (!user) {
return res.status(404).json({ success: false, message: "Email not found" });
}
// Return accessToken and user info
return res.status(200).json({
success: true,
email: user.userDetails?.email,
message: "Email ID is already registerd"
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { client_verify_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,10 +69,9 @@ 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);
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server 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' });
}
// ✅ 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 });
}
// ✅ 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 {
@ -126,7 +102,7 @@ async function process_reset(req, res) {
await passwordResetService.resetPassword(token, password, userType);
// Render success page
res.render('auth/reset-password-success', {userType});
res.render('auth/reset-password-success');
} catch (error) {
logger.error(`PasswordResetController - Process reset error: ${error.message}`);

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

@ -1,106 +0,0 @@
import { ProjectModel, VAModel, ClientModel } from "../../models/vaModel.js";
import { Types } from "mongoose";
import { updateEntityRelationships } from "../../helper/updateEntityRelationships.js";
// 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;
// Validate required fields
if (!name) {
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"
});
}
// Validate and convert clientId to ObjectId
let clientObjectId;
try {
clientObjectId = new Types.ObjectId(clientId);
} catch (error) {
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"
// });
// }
// }
// }
// 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
};
const created = await ProjectModel.create(toCreate);
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
const relationshipUpdate = await updateEntityRelationships({
project: {
projectId: created._id,
clientId: clientObjectId,
vaId: vaAssignedArray
}
});
if (!relationshipUpdate.success) {
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
}
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
} catch (error) {
console.error("project_add_sf error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { project_add_sf };

View File

@ -1,254 +0,0 @@
import { ProjectModel } from "../../models/vaModel.js";
import { Types } from "mongoose";
import { updateEntityRelationships, removeEntityRelationships } from "../../helper/updateEntityRelationships.js";
/**
* Update project details - PATCH endpoint
* Supports updating: status, VA assignments, hourly rate, dedicated hours, etc.
*/
async function update_project_sf(req, res) {
try {
const { projectId } = req.params;
const payload = req.body || {};
const {
name,
hourlyRate,
vaAssigned,
dedicatedHours,
status,
totalLoggedHours,
vaWorkingScheduleStart,
vaWorkingScheduleEnd,
isFlexibleAllowed,
vaWorkingDelayAllowedStart,
vaWorkingDelayAllowedEnd
} = payload;
// 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"
});
}
// Prepare update data
const updateData = {};
let relationshipUpdate = null;
let vaRelationshipUpdate = null;
// Update name if provided
if (name !== undefined) {
if (!name.trim()) {
return res.status(400).json({
success: false,
message: "Project name cannot be empty"
});
}
updateData.name = name.trim();
}
// Update hourly rate if provided
if (hourlyRate !== undefined) {
updateData.hourlyRate = hourlyRate;
}
// Update dedicated hours if provided
if (dedicatedHours !== undefined) {
updateData.dedicatedHours = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"];
}
// Update total logged hours if provided
if (totalLoggedHours !== undefined) {
updateData.totalLoggedHours = parseFloat(totalLoggedHours) || 0;
}
// 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"
});
}
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);
// 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)
}
});
} catch (error) {
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"
});
}
}
}
// updateData.vaAssigned = newVaAssignedArray;
// Calculate VAs to add and remove for relationship updates
const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.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()));
// Update relationships for added VAs
if (vaToAdd.length > 0) {
relationshipUpdate = await updateEntityRelationships({
project: {
projectId: projectObjectId,
clientId: existingProject.clientId,
vaId: vaToAdd
}
});
}
// Remove relationships for removed VAs
if (vaToRemove.length > 0) {
vaRelationshipUpdate = await removeEntityRelationships({
project: {
projectId: projectObjectId,
clientId: existingProject.clientId,
vaId: vaToRemove
}
});
}
}
// If no fields to update
if (Object.keys(updateData).length === 0) {
return res.status(400).json({
success: false,
message: "No valid fields to update"
});
}
// Update the project
const updatedProject = await ProjectModel.findByIdAndUpdate(
projectObjectId,
updateData,
{ new: true, runValidators: true }
).populate('clientId', 'userDetails.name userDetails.email')
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
// If VA assignments were changed but relationship update failed, log warning
if (vaAssigned !== undefined) {
if (relationshipUpdate && !relationshipUpdate.success) {
console.warn('Project updated but VA relationship addition failed:', relationshipUpdate.message);
}
if (vaRelationshipUpdate && !vaRelationshipUpdate.success) {
console.warn('Project updated but VA relationship removal failed:', vaRelationshipUpdate.message);
}
}
return res.status(200).json({
success: true,
message: "Project updated successfully",
data: updatedProject,
relationships: {
added: relationshipUpdate,
removed: vaRelationshipUpdate
}
});
} 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"
});
}
// MongoDB validation error
if (error.name === 'ValidationError') {
const errors = Object.values(error.errors).map(err => err.message);
return res.status(400).json({
success: false,
message: "Validation error",
errors: errors
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export {
update_project_sf
};

View File

@ -1,102 +1,6 @@
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 }
// ]
// });
// 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",
// 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,
// }
// 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)
// });
// 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 {
// 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;
// }
// // return {
// // closedWorkTracks: openWorkTracks.length,
// // logoutLogId: logoutLog._id,
// // };
// } catch (error) {
// console.error("Error closing previous session:", error);
// throw error;
// }
// }
import { VAModel } from "../../../models/vaModel.js";
// Login VA
async function va_login_callback(req, res) {
@ -128,22 +32,6 @@ async function va_login_callback(req, res) {
return res.status(401).json({ success: false, message: "Invalid password" });
}
// 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 });
// 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);
// }
// Generate JWT accessToken with 24h expiry
const accessToken = jwt.sign(
{
@ -152,7 +40,7 @@ async function va_login_callback(req, res) {
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey",
// { expiresIn: "24h" }
{ expiresIn: "24h" }
);
// Generate JWT refreshToken with 24h expiry
@ -163,64 +51,29 @@ async function va_login_callback(req, res) {
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
// { expiresIn: "30d" }
{ expiresIn: "30d" }
);
await VAModel.updateOne({ "userDetails.email": userDetails.email }, {
$set: { accessToken, refreshToken }
})
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 {
responseData = {
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
}
}
// }
// Return accessToken and user info
return res.status(200).json(responseData);
return res.status(200).json({
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
},
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Helper function to format session duration
function formatSessionDuration(loginTime) {
const now = new Date();
const login = new Date(loginTime);
const durationMs = now - login;
const hours = Math.floor(durationMs / (1000 * 60 * 60));
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((durationMs % (1000 * 60)) / 1000);
return `${hours}h ${minutes}m ${seconds}s`;
}
export { va_login_callback };

View File

@ -1,32 +0,0 @@
import { VAModel } from "../../../models/vaModel.js";
// Verify VA Email
async function va_verify_email(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { email } = payload;
if (!email) {
return res.status(400).json({ success: false, message: "Please provide email" });
}
// Find user by email
const user = await VAModel.findOne({ "userDetails.email": email });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid Email ID" });
}
// Return accessToken and user info
return res.status(200).json({
success: true,
email: user.userDetails?.email,
message: "Email verified successfully"
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_verify_email };

View File

@ -1,101 +0,0 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Get all leave lists applied by VA
async function va_leave_list_function(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;
// Get parameters from route params (they are optional)
const { status, type } = req.params;
// Get sorting from query params (optional)
const {
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
// Build filter object
const filter = { vaId };
// Handle status filter from params
if (status && status !== 'ALL') {
filter.status = status.toUpperCase();
}
// Handle type filter from params (using 'type' instead of 'leaveType')
if (type && type !== 'ALL') {
filter.leaveType = type.toUpperCase();
}
// Validate sort parameters
const validSortFields = ['createdAt', 'dateFrom', 'dateTo', 'status', 'leaveType'];
const sortField = validSortFields.includes(sortBy) ? sortBy : 'createdAt';
const sortDirection = sortOrder.toLowerCase() === 'asc' ? 1 : -1;
// Get all leaves without pagination
const leaves = await LeavesModel.find(filter)
.sort({ [sortField]: sortDirection })
.select('-__v')
.lean();
// Format the response data
const formattedLeaves = leaves.map(leave => ({
leaveId: leave._id,
dateFrom: leave.dateFrom.toISOString().split('T')[0],
dateTo: leave.dateTo.toISOString().split('T')[0],
totalDays: parseInt(leave.days),
leaveType: leave.leaveType,
comment: leave.comment,
status: leave.status,
appliedOn: leave.createdAt,
reviewedAt: leave.reviewedAt || null,
rejectionReason: leave.rejectionReason || null
}));
return res.status(200).json({
success: true,
message: "Leave list retrieved successfully",
data: {
leaves: formattedLeaves,
totalCount: formattedLeaves.length
}
});
} catch (error) {
console.error("Leave list 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,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_list_function };

View File

@ -1,114 +0,0 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Approve or reject leave
async function va_leave_manage_function(req, res) {
try {
const { leaveId } = req.params;
const { action, rejectionReason } = req.body;
if (!leaveId) {
return res.status(400).json({
success: false,
message: "Leave ID is required"
});
}
if (!action || !['APPROVE', 'REJECT'].includes(action.toUpperCase())) {
return res.status(400).json({
success: false,
message: "Action is required and must be either 'APPROVE' or 'REJECT'"
});
}
// If rejecting, rejectionReason is required
if (action.toUpperCase() === 'REJECT' && !rejectionReason) {
return res.status(400).json({
success: false,
message: "Rejection reason is required when rejecting a leave"
});
}
// Find the leave request
const leaveRequest = await LeavesModel.findById(leaveId);
if (!leaveRequest) {
return res.status(404).json({
success: false,
message: "Leave request not found"
});
}
// Check if leave is already processed
if (leaveRequest.status !== 'PENDING') {
return res.status(400).json({
success: false,
message: `Leave request is already ${leaveRequest.status.toLowerCase()}`
});
}
// Prepare update data
const updateData = {
status: action.toUpperCase() === 'APPROVE' ? 'APPROVED' : 'REJECTED',
reviewedAt: new Date()
};
// Add rejection reason if rejecting
if (action.toUpperCase() === 'REJECT') {
updateData.rejectionReason = rejectionReason;
}
// Update the leave request
const updatedLeave = await LeavesModel.findByIdAndUpdate(
leaveId,
updateData,
{ new: true } // Return updated document
).select('-__v').lean();
// Format the response
const formattedLeave = {
leaveId: updatedLeave._id,
dateFrom: updatedLeave.dateFrom.toISOString().split('T')[0],
dateTo: updatedLeave.dateTo.toISOString().split('T')[0],
totalDays: parseInt(updatedLeave.days),
leaveType: updatedLeave.leaveType,
comment: updatedLeave.comment,
status: updatedLeave.status,
appliedOn: updatedLeave.createdAt,
reviewedAt: updatedLeave.reviewedAt,
rejectionReason: updatedLeave.rejectionReason || null
};
return res.status(200).json({
success: true,
message: `Leave request ${action.toLowerCase()}d successfully`,
data: formattedLeave
});
} catch (error) {
console.error("Leave approve/reject 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" });
}
if (error.name === 'CastError') {
return res.status(400).json({
success: false,
message: "Invalid leave ID format"
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_manage_function };

View File

@ -1,357 +0,0 @@
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" });
}
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 { 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"
});
}
// Validate date formats
let startDate, endDate;
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)"
});
}
} catch (error) {
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"
});
}
// 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"
});
}
// 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"
});
}
// Calculate total days (inclusive of both start and end dates)
const timeDifference = endDate.getTime() - startDate.getTime();
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"
});
}
// Check for overlapping leave requests
const existingLeave = await LeavesModel.findOne({
vaId,
status: "PENDING",
$or: [
{
dateFrom: { $lte: endDate },
dateTo: { $gte: startDate }
}
]
});
if (existingLeave) {
return res.status(400).json({
success: false,
message: "You already have a pending leave request that overlaps with these dates"
});
}
// Check for overlapping approved leaves
const existingApprovedLeave = await LeavesModel.findOne({
vaId,
status: "APPROVED",
$or: [
{
dateFrom: { $lte: endDate },
dateTo: { $gte: startDate }
}
]
});
if (existingApprovedLeave) {
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,
dateFrom: startDate,
dateTo: endDate,
days: totalDays.toString(),
leaveType,
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
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": ""
}
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 })
});
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;
}
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,
status: "PENDING"
}
});
} 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,
message: "Internal Server Error",
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

@ -3,138 +3,34 @@ import { VAModel, LoggedModel } from "../../../models/vaModel.js";
// Getting all active VA's and their details
async function va_callback(req, res) {
try {
// Find the latest log entry for each VA
// Find the latest log entry for each VA and return those whose last entry is LOGIN
const agg = await LoggedModel.aggregate([
{ $sort: { time: -1 } },
{
$group: {
_id: "$vaId",
lastLog: { $first: "$$ROOT" },
totalLogins: {
$sum: {
$cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0]
}
},
lastActivity: { $first: "$time" }
}
},
{
$match: {
"lastLog.loggedType": "LOGIN"
}
}
{ $group: { _id: "$vaId", lastLog: { $first: "$$ROOT" } } },
{ $match: { "lastLog.loggedType": "LOGIN" } }
]);
const vaIds = agg.map((a) => a._id).filter(Boolean);
if (vaIds.length === 0) {
return res.status(200).json({
success: true,
count: 0,
data: [],
message: "No active VAs found"
});
return res.status(200).json({ success: true, count: 0, data: [] });
}
// Get VA details with additional fields
const vas = await VAModel.find({ _id: { $in: vaIds } })
.select('userDetails.name userDetails.email userDetails.phone status createdAt')
.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
};
const vas = await VAModel.find({ _id: { $in: vaIds } }).lean().exec();
// map lastLog by VA id for easy attachment
const lastLogByVa = agg.reduce((acc, a) => {
acc[String(a._id)] = a.lastLog;
return acc;
}, {});
// Calculate session duration for currently active VAs
const currentTime = new Date();
const results = vas.map((v) => {
const activity = activityByVa[String(v._id)] || {};
const lastLog = activity.lastLog;
// Calculate current session duration if still logged in
let currentSessionDuration = null;
let isCurrentlyActive = false;
if (lastLog && lastLog.loggedType === "LOGIN") {
const loginTime = new Date(lastLog.time);
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); // in seconds
isCurrentlyActive = true;
// Consider VA inactive if last login was more than 24 hours ago
const hoursSinceLastActivity = (currentTime - loginTime) / (1000 * 60 * 60);
if (hoursSinceLastActivity > 24) {
isCurrentlyActive = false;
}
}
return {
id: v._id,
name: v.userDetails?.name || 'Unknown',
email: v.userDetails?.email,
phone: v.userDetails?.phone,
status: v.status || 'active',
isCurrentlyActive: isCurrentlyActive,
lastLoginTime: lastLog?.time || null,
lastActivity: activity.lastActivity,
currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionDuration ? formatTime(currentSessionDuration) : null,
totalLogins: activity.totalLogins || 0,
lastProject: lastLog?.projectId || null,
vaSince: v.createdAt
};
});
// Separate active and inactive VAs
const activeVAs = results.filter(va => va.isCurrentlyActive);
const inactiveButLoggedIn = results.filter(va => !va.isCurrentlyActive);
return res.status(200).json({
success: true,
count: {
total: results.length,
active: activeVAs.length,
inactive: inactiveButLoggedIn.length
},
data: {
active: activeVAs,
inactive: inactiveButLoggedIn
},
summary: {
currentlyWorking: activeVAs.length,
totalLoggedIn: results.length,
lastUpdated: currentTime.toISOString()
}
});
const results = vas.map((v) => ({ ...v, lastLog: lastLogByVa[String(v._id)] || null }));
return res.status(200).json({ success: true, count: results.length, data: results });
} catch (error) {
console.error("VA callback error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
// Helper function to format time
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${remainingSeconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${remainingSeconds}s`;
} else {
return `${remainingSeconds}s`;
return res.status(500).send("Internal Server Error");
}
}

View File

@ -1,299 +1,114 @@
import jwt from "jsonwebtoken";
import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day
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 };
}
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) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
// // 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
// });
// 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 };
// }
// }
// 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 accessToken = req.headers.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" });
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 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,
});
// }
}
// // 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);
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))
// }
// };
// // 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({
success: true,
message: "Work tracking stopped",
data: {
id: workEntry._id,
from: workEntry.from,
to: workEntry.to,
formattedTotalTime,
totalTimeMs
},
});
}
// 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,24 +1,6 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
// Helper function to parse date string in DD-MM-YYYY format
function parseDate(dateString) {
if (!dateString) return null;
const parts = dateString.split('-');
if (parts.length !== 3) return null;
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // Months are 0-indexed in JavaScript
const year = parseInt(parts[2], 10);
// Validate date components
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
if (day < 1 || day > 31 || month < 0 || month > 11 || year < 1900) return null;
return new Date(year, month, day);
}
// Getting logged hours in a specific project of VA
async function va_log_work_report_callback(req, res) {
try {
@ -45,27 +27,15 @@ async function va_log_work_report_callback(req, res) {
const payload = req.body || {};
const { fromDate, toDate } = payload;
let start = fromDate ? parseDate(fromDate) : new Date();
let end = toDate ? parseDate(toDate) : new Date();
if ((fromDate && (!start || isNaN(start.getTime()))) || (toDate && (!end || isNaN(end.getTime())))) {
return res.status(400).json({
success: false,
message: "Invalid Date format. Use DD-MM-YYYY"
});
}
const start = fromDate ? new Date(fromDate) : new Date();
const end = toDate ? new Date(toDate) : new Date();
if (!fromDate && !toDate) {
// If no dates provided, default to today
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
} else {
// If dates are provided, set proper time boundaries
if (fromDate) start.setHours(0, 0, 0, 0);
if (toDate) end.setHours(23, 59, 59, 999);
}
let logs = await LoggedModel.find({
const logs = await LoggedModel.find({
vaId,
time: { $gte: start, $lte: end },
})
@ -85,56 +55,28 @@ async function va_log_work_report_callback(req, res) {
let totalWorkSeconds = 0;
const projectWork = {}; // key: projectId -> seconds
// Group logs by date (YYYY-MM-DD format)
const logsByDate = {};
logs.forEach(log => {
const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD
if (!logsByDate[date]) {
logsByDate[date] = [];
}
logsByDate[date].push(log);
});
let lastLogin = null;
let lastProjectId = null;
// Process each day separately
for (const [date, dayLogs] of Object.entries(logsByDate)) {
// Check if this day has any adjustment logs
const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
let dayWorkSeconds = 0;
let lastLogin = null;
let lastProjectId = null;
// ⏱️ Calculate work duration
for (const log of logs) {
const time = new Date(log.time);
// If day has adjustment logs, only process adjustment logs
// If no adjustment logs, process all logs for that day
const logsToProcess = hasAdjustmentLogs
? dayLogs.filter(log => log.isAdjustment === true)
: dayLogs;
if (log.loggedType === "LOGIN") {
lastLogin = time;
lastProjectId = log.projectId?._id?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
totalWorkSeconds += diffSec;
// Sort logs by time for proper pairing (already sorted by query, but ensure)
logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
// Calculate work duration for this day
for (const log of logsToProcess) {
const time = new Date(log.time);
if (log.loggedType === "LOGIN") {
lastLogin = time;
lastProjectId = log.projectId?._id?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
if (lastProjectId) {
projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec;
}
// reset after each pair
lastLogin = null;
lastProjectId = null;
if (lastProjectId) {
projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec;
}
}
totalWorkSeconds += dayWorkSeconds;
// reset after each pair
lastLogin = null;
lastProjectId = null;
}
}
// 🕓 Convert total seconds to hh:mm:ss
@ -167,9 +109,8 @@ async function va_log_work_report_callback(req, res) {
});
} catch (error) {
console.log(error)
return res.status(500).send("Internal Server Error");
}
}
export { va_log_work_report_callback };
export { va_log_work_report_callback };

View File

@ -0,0 +1,151 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
// Adjust missing hours by creating new records
async function va_timesheet_adjustment_function(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 { projectId, date } = req.params;
const { loginTime, logoutTime, reason } = req.body;
if (!projectId || !date) {
return res.status(400).json({ success: false, message: "Project ID and date are required" });
}
if (!loginTime || !logoutTime) {
return res.status(400).json({ success: false, message: "Login time and logout time are required" });
}
// Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({
_id: projectId,
vaAssigned: vaId
});
if (!project) {
return res.status(404).json({ success: false, message: "Project not found or VA not assigned to this project" });
}
// Parse and validate times
const adjustmentDate = new Date(date);
const loginDateTime = new Date(loginTime);
const logoutDateTime = new Date(logoutTime);
// Ensure times are within the target date
const startOfDay = new Date(adjustmentDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(adjustmentDate);
endOfDay.setHours(23, 59, 59, 999);
if (loginDateTime < startOfDay || loginDateTime > endOfDay ||
logoutDateTime < startOfDay || logoutDateTime > endOfDay) {
return res.status(400).json({
success: false,
message: "Login and logout times must be within the specified date"
});
}
if (loginDateTime >= logoutDateTime) {
return res.status(400).json({
success: false,
message: "Login time must be before logout time"
});
}
// Check for existing logs that might conflict with this adjustment
const existingLogs = await LoggedModel.find({
vaId,
projectId,
time: {
$gte: loginDateTime,
$lte: logoutDateTime
}
});
if (existingLogs.length > 0) {
return res.status(400).json({
success: false,
message: "There are existing logs in the specified time range. Please choose a different time range."
});
}
// Create adjustment logs
const loginLog = new LoggedModel({
loggedType: "LOGIN",
time: loginDateTime,
vaId,
vaEmail,
projectId,
isAdjustment: true,
adjustmentReason: reason
});
const logoutLog = new LoggedModel({
loggedType: "LOGOUT",
time: logoutDateTime,
vaId,
vaEmail,
projectId,
isAdjustment: true,
adjustmentReason: reason
});
// Save both logs in transaction
await loginLog.save();
await logoutLog.save();
// Calculate adjusted hours
const adjustedSeconds = (logoutDateTime - loginDateTime) / 1000;
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
};
const response = {
success: true,
message: "Timesheet adjustment completed successfully",
data: {
adjustment: {
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
adjustedHoursDecimal: (adjustedSeconds / 3600).toFixed(2),
reason: reason
},
logs: {
loginLogId: loginLog._id,
logoutLogId: logoutLog._id
}
}
};
return res.status(201).json(response);
} catch (error) {
console.error("Timesheet adjustment error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
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) {
@ -100,7 +86,7 @@ async function va_timesheet_summary_function(req, res) {
if (!firstLoginTime || logTime < firstLoginTime) {
firstLoginTime = logTime;
}
} else if (log.loggedType === "LOGOUT") {
} else if (log.loggedType === "LOGOUT" && lastLogin) {
if (loginStack.length > 0) {
// Pop the last login from stack
const lastLogin = loginStack.pop();
@ -163,16 +149,15 @@ 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);
const response = {
success: true,
data: {
date: targetDate.toISOString().split('T')[0],
project: {
id: project._id,
name: project.name,
@ -186,10 +171,10 @@ async function va_timesheet_summary_function(req, res) {
summary: {
totalWorkedHours: formatTime(totalWorkSeconds),
totalWorkedHoursDetailed: formatTimeDetailed(totalWorkSeconds),
totalWorkedHoursDecimal: totalWorkedHours.toFixed(2),
totalWorkedHoursDecimal: totalWorkedHours.toFixed(4),
totalWorkedMinutes: (totalWorkSeconds / 60).toFixed(2),
dedicatedHours: dedicatedHours,
pendingHours: pendingHours.toFixed(2),
pendingHours: pendingHours.toFixed(4),
firstLoginTime: firstLoginTime ? firstLoginTime.toISOString() : null,
lastLogoutTime: lastLogoutTime ? lastLogoutTime.toISOString() : null
},

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

@ -34,7 +34,7 @@ async function va_profile_callback(req, res) {
return res.status(200).json({
success: true,
message: "VA profile",
data: vaUser.userDetails
data: vaUser
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });

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" });
}
// Allowed fields to update
const allowedFields = [
"firstName", "lastName", "dob", "age", "address",
"city", "state", "country", "phone", "skype",
"paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail"
"bankName", "accountHolderName", "accountNumber", "routingNumber"
];
let hasUpdates = false;
const updatedFields = [];
// Update allowed fields if provided
// Update allowed fields only
allowedFields.forEach(field => {
if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') {
if (userDetails[field] !== undefined) {
vaUser.userDetails[field] = userDetails[field];
hasUpdates = true;
updatedFields.push(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,22 +78,27 @@ 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) {
return res.status(400).json({
success: false,
message: "No valid fields to update",
});
}
// if (userDetails.password) {
// if (!userDetails.cPassword) {
// return res.status(400).json({
// success: false,
// message: "Please confirm your new password",
// });
// }
if (status) {
vaUser.status = status;
updatedFields.push('status');
}
// if (userDetails.password !== userDetails.cPassword) {
// return res.status(400).json({
// success: false,
// message: "Passwords do not match",
// });
// }
// const saltRounds = 10;
// const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
// vaUser.userDetails.password = hashedPassword;
// }
await vaUser.save();
@ -149,7 +110,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,
},
@ -49,38 +47,7 @@ async function va_register(req, res) {
const created = await VAModel.create(toCreate);
// Send data to Salesforce API after successful registration
try {
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, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
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 });
} 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;
throw salesforceError;
}
return res.status(201).json({ success: true, data: created });
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });

View File

@ -1,582 +1,16 @@
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";
// Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) {
const start = new Date(date);
start.setUTCHours(0, 0, 0, 0);
const end = new Date(date);
end.setUTCHours(23, 59, 59, 999);
return { start, end };
}
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) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
// 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()) {
try {
const currentTime = logTime;
// 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`);
// 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",
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,
}
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)
});
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 {
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;
}
} catch (error) {
console.error("Error closing previous session:", error);
throw error;
}
}
import { LoggedModel } from "../../../models/vaModel.js";
// Getting logged hours in a specific project of VA
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);
const { vaId, vaEmail, projectId, type } = 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 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)
if (type === "LOGIN") {
const { start, end } = getDayRange(); // UTC day range
// 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]) {
return res.status(400).json({
success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours[dayNumber])})`,
// warnings: scheduleCheck.warnings
});
}
// Check last log entry
const lastLog = await LoggedModel.findOne({ vaId })
.sort({ time: -1 })
.lean();
// Prevent login if last action was login (not logout)
if (lastLog && lastLog.loggedType === "LOGIN") {
// 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);
// 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
});
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,
}
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
});
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',
// 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);
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;
}
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
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);
@ -10,21 +9,6 @@ function formatHMS(ms) {
return `${h}h ${m}m ${s}s`;
}
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) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
async function va_projects_log_list_function(req, res) {
try {
const { projectId, from, to } = req.params;
@ -68,12 +52,13 @@ async function va_projects_log_list_function(req, res) {
projectList = [project];
} else {
const all = await ProjectModel.find().lean();
console.log(all.map(p => p.vaAssigned));
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,17 +96,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",
totalLogs: logs.length,
status: project?.status
totalLogs: logs.length
});
}

View File

@ -1,4 +1,3 @@
import jwt from "jsonwebtoken";
import { LoggedModel, WorkTrackModel, VAModel } from "../../../models/vaModel.js";
function formatHMS(ms) {
@ -11,36 +10,6 @@ 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 { projectId, from, to } = req.params;
if (!projectId) {
@ -49,76 +18,19 @@ 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({
projectId,
vaId,
time: { $gte: fromDate, $lte: toDate }
}).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 +41,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,80 +0,0 @@
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;
// 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);
}
const adjustments = await AdjustedHoursModel.find(filter)
.sort({ createdAt: -1 })
.populate('vaId', 'userDetails.name userDetails.email')
.populate('projectId', 'name')
.lean();
// Format response
const formattedData = adjustments.map(adj => ({
id: adj._id,
va: {
id: adj.vaId._id,
name: adj.vaId.userDetails?.name,
email: adj.vaId.userDetails?.email
},
project: {
id: adj.projectId._id,
name: adj.projectId.name
},
date: adj.date,
loginTime: adj.loginTime,
logoutTime: adj.logoutTime,
comment: adj.comment,
status: adj.status,
requestedAt: adj.createdAt,
reviewedAt: adj.reviewedAt,
rejectionReason: adj.rejectionReason,
adjustedHours: formatTime((new Date(adj.logoutTime) - new Date(adj.loginTime)) / 1000)
}));
return res.status(200).json({
success: true,
data: formattedData,
pagination: {
currentPage: adjustments.page,
totalPages: adjustments.totalPages,
totalItems: adjustments.totalDocs,
hasNext: adjustments.hasNextPage,
hasPrev: adjustments.hasPrevPage
}
});
} catch (error) {
console.error("Get adjustment requests error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
export { get_adjustment_requests_list }

View File

@ -1,122 +0,0 @@
import { AdjustedHoursModel, LoggedModel } from "../../../models/vaModel.js";
// Approve or reject adjustment request
async function va_timesheet_manage_adjustment_request(req, res) {
try {
const { adjustmentId } = req.params;
const { action, rejectionReason } = req.body; // action: "approve" or "reject"
if (!adjustmentId || !action) {
return res.status(400).json({
success: false,
message: "Adjustment ID and action are required"
});
}
// Find adjustment request
const adjustment = await AdjustedHoursModel.findById(adjustmentId)
.populate('vaId', 'userDetails.email');
if (!adjustment) {
return res.status(404).json({
success: false,
message: "Adjustment request not found"
});
}
if (adjustment.status !== "PENDING") {
return res.status(400).json({
success: false,
message: "Adjustment request has already been processed"
});
}
if (action === "APPROVE") {
// Create new log entries in LoggedModel
const loginLog = new LoggedModel({
loggedType: "LOGIN",
time: adjustment.loginTime,
vaId: adjustment.vaId._id,
vaEmail: adjustment.vaId.userDetails.email,
projectId: adjustment.projectId,
isAdjustment: true,
adjustmentId: adjustment._id
});
const logoutLog = new LoggedModel({
loggedType: "LOGOUT",
time: adjustment.logoutTime,
vaId: adjustment.vaId._id,
vaEmail: adjustment.vaId.userDetails.email,
projectId: adjustment.projectId,
isAdjustment: true,
adjustmentId: adjustment._id
});
await loginLog.save();
await logoutLog.save();
// Update adjustment record
adjustment.status = "APPROVED";
adjustment.reviewedAt = new Date();
adjustment.adjustedLogs = [
{ loginLogId: loginLog._id },
{ logoutLogId: logoutLog._id }
];
await adjustment.save();
return res.status(200).json({
success: true,
message: "Adjustment request approved successfully",
data: {
adjustmentId: adjustment._id,
status: "APPROVED",
loginLogId: loginLog._id,
logoutLogId: logoutLog._id,
adjustedHours: formatTime((adjustment.logoutTime - adjustment.loginTime) / 1000)
}
});
} else if (action === "REJECT") {
// Update adjustment record as rejected
adjustment.status = "REJECTED";
adjustment.reviewedAt = new Date();
adjustment.rejectionReason = rejectionReason || "No reason provided";
await adjustment.save();
return res.status(200).json({
success: true,
message: "Adjustment request rejected",
data: {
adjustmentId: adjustment._id,
status: "REJECTED",
rejectionReason: adjustment.rejectionReason
}
});
} else {
return res.status(400).json({
success: false,
message: "Invalid action. Use 'approve' or 'reject'"
});
}
} catch (error) {
console.error("Manage adjustment error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
export { va_timesheet_manage_adjustment_request }

View File

@ -1,335 +0,0 @@
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) {
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 { projectId, date, loginTime, logoutTime, comment } = req.body;
if (!projectId || !date || !loginTime || !logoutTime || !comment) {
return res.status(400).json({
success: false,
message: "Project ID, date, loginTime, logoutTime, and comment are required"
});
}
// Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({
_id: projectId,
"vaAssigned.vaId": vaId
});
if (!project) {
return res.status(404).json({ success: false, message: "Project not found or VA not assigned to this project" });
}
let adjustmentDate;
try {
adjustmentDate = new Date(date);
if (isNaN(adjustmentDate.getTime())) {
return res.status(400).json({
success: false,
message: "Invalid date format. Use YYYY-MM-DD"
});
}
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid date format"
});
}
const combineDateTime = (dateObj, timeStr) => {
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
const combined = new Date(dateObj);
combined.setHours(hours, minutes, seconds, 0);
return combined;
};
const loginDateTime = combineDateTime(adjustmentDate, loginTime);
const logoutDateTime = combineDateTime(adjustmentDate, logoutTime);
if (isNaN(adjustmentDate.getTime()) || isNaN(loginDateTime.getTime()) || isNaN(logoutDateTime.getTime())) {
return res.status(400).json({
success: false,
message: "Invalid date format"
});
}
// Ensure login time is before logout time
if (loginDateTime >= logoutDateTime) {
return res.status(400).json({
success: false,
message: "Login time must be before logout time"
});
}
// Ensure times are within the target date
const startOfDay = new Date(adjustmentDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(adjustmentDate);
endOfDay.setHours(23, 59, 59, 999);
if (loginDateTime < startOfDay || loginDateTime > endOfDay ||
logoutDateTime < startOfDay || logoutDateTime > endOfDay) {
return res.status(400).json({
success: false,
message: "Login and logout times must be within the specified date"
});
}
const existingAdjustment = await AdjustedHoursModel.findOne({
vaId,
projectId,
date: startOfDay,
status: "PENDING"
});
if (existingAdjustment) {
return res.status(400).json({
success: false,
message: "You already have a pending adjustment request for this date and project"
});
}
const processedComment = processHTMLComment(comment);
const adjustmentRequest = new AdjustedHoursModel({
vaId,
projectId,
date: startOfDay,
loginTime: loginDateTime,
logoutTime: logoutDateTime,
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
status: "PENDING"
});
await adjustmentRequest.save();
const adjustedSeconds = (logoutDateTime - loginDateTime) / 1000;
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
};
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
const sfData = {
"id": adjustmentRequest._id.toString(),
"va": {
"id": vaId.toString()
},
"project": {
"id": projectId.toString()
},
"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
"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})
});
if (!response.ok) {
const errorText = await response.text();
await AdjustedHoursModel.findByIdAndDelete(adjustmentRequest._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: "Adjustment request submitted successfully",
data: {
adjustmentId: adjustmentRequest._id,
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
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;
}
// 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"
// }
// });
} catch (error) {
console.error("Timesheet adjustment error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
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,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

@ -1,346 +0,0 @@
import { ClientModel, VAModel, ProjectModel } from "../models/vaModel.js";
/**
* Generic function to update relationships between Client, VA, and Project
* @param {Object} data - The update data object
* @returns {Object} - Result of the update operations
*/
async function updateEntityRelationships(data) {
try {
let results = {};
// Handle Project updates
if (data.project) {
const { projectId, vaId, clientId } = data.project;
if (!projectId) {
throw new Error('Project ID is required for project updates');
}
// Update Client with projectId
if (clientId) {
const clientUpdate = await ClientModel.findByIdAndUpdate(
clientId,
{
$addToSet: {
projectIds: projectId,
vaIds: vaId
}
},
{ new: true }
);
results.clientUpdate = clientUpdate ? 'Client updated successfully' : 'Client not found';
}
// Update VAs with projectId
if (vaId && Array.isArray(vaId) && vaId.length > 0) {
const vaUpdateResult = await VAModel.updateMany(
{ _id: { $in: vaId } },
{
$addToSet: {
projectIds: projectId,
clientIds: clientId
}
}
);
results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`;
}
// // Update Project with clientId and vaId
// const projectUpdateData = {};
// if (clientId) {
// projectUpdateData.clientId = clientId;
// }
// // 🐞🐞🐞 Here Duplicate Entry of Va is Created if SF pass same va multiple times in same req
// if (vaId && Array.isArray(vaId)) {
// projectUpdateData.vaAssigned = vaId;
// }
// if (Object.keys(projectUpdateData).length > 0) {
// const projectUpdate = await ProjectModel.findByIdAndUpdate(
// projectId,
// projectUpdateData,
// { new: true }
// );
// results.projectUpdate = projectUpdate ? 'Project updated successfully' : 'Project not found';
// }
if (vaId && Array.isArray(vaId)) {
await ProjectModel.findByIdAndUpdate(
projectId,
{ $addToSet: { vaAssigned: { $each: vaId } } }
);
}
// Update other fields normally
const projectUpdateData = {};
if (clientId) {
projectUpdateData.clientId = clientId;
}
if (Object.keys(projectUpdateData).length > 0) {
const projectUpdate = await ProjectModel.findByIdAndUpdate(
projectId,
projectUpdateData,
{ new: true }
);
results.projectUpdate = projectUpdate ? 'Project updated successfully' : 'Project not found';
}
}
// Handle VA updates
if (data.va) {
const { vaId, projectId, clientId } = data.va;
if (!vaId) {
throw new Error('VA ID is required for VA updates');
}
// Update Projects with vaId
if (projectId && Array.isArray(projectId) && projectId.length > 0) {
const projectUpdateResult = await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{
$addToSet: {
vaAssigned: vaId
}
}
);
results.projectUpdate = `Updated ${projectUpdateResult.modifiedCount} projects`;
}
// Update Clients with vaId
if (clientId && Array.isArray(clientId) && clientId.length > 0) {
const clientUpdateResult = await ClientModel.updateMany(
{ _id: { $in: clientId } },
{
$addToSet: {
vaIds: vaId
}
}
);
results.clientUpdate = `Updated ${clientUpdateResult.modifiedCount} clients`;
}
// Update VA with clientId and projectId
const vaUpdateData = {};
if (clientId && Array.isArray(clientId)) {
vaUpdateData.clientIds = clientId;
}
if (projectId && Array.isArray(projectId)) {
vaUpdateData.projectIds = projectId;
}
if (Object.keys(vaUpdateData).length > 0) {
const vaUpdate = await VAModel.findByIdAndUpdate(
vaId,
{ $addToSet: vaUpdateData },
{ new: true }
);
results.vaUpdate = vaUpdate ? 'VA updated successfully' : 'VA not found';
}
}
// Handle Client updates
if (data.client) {
const { clientId, projectId, vaId } = data.client;
if (!clientId) {
throw new Error('Client ID is required for client updates');
}
// Update Projects with clientId
if (projectId && Array.isArray(projectId) && projectId.length > 0) {
const projectUpdateResult = await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{ clientId: clientId }
);
results.projectUpdate = `Updated ${projectUpdateResult.modifiedCount} projects`;
}
// Update VAs with clientId
if (vaId && Array.isArray(vaId) && vaId.length > 0) {
const vaUpdateResult = await VAModel.updateMany(
{ _id: { $in: vaId } },
{
$addToSet: {
clientIds: clientId
}
}
);
results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`;
}
// Update Client with projectId and vaId
const clientUpdateData = {};
if (projectId && Array.isArray(projectId)) {
clientUpdateData.projectIds = projectId;
}
if (vaId && Array.isArray(vaId)) {
clientUpdateData.vaIds = vaId;
}
if (Object.keys(clientUpdateData).length > 0) {
const clientUpdate = await ClientModel.findByIdAndUpdate(
clientId,
{ $addToSet: clientUpdateData },
{ new: true }
);
results.clientUpdate = clientUpdate ? 'Client updated successfully' : 'Client not found';
}
}
return {
success: true,
message: 'Relationships updated successfully',
results
};
} catch (error) {
console.error('Error updating entity relationships:', error);
return {
success: false,
message: error.message,
results: {}
};
}
}
/**
* Remove relationships (for deletion cases)
* @param {Object} data - The removal data object
* @returns {Object} - Result of the removal operations
*/
async function removeEntityRelationships(data) {
try {
let results = {};
// Handle Project removal
if (data.project) {
const { projectId, vaId, clientId } = data.project;
if (!projectId) {
throw new Error('Project ID is required for project removal');
}
// Remove projectId from Client
if (clientId) {
await ClientModel.findByIdAndUpdate(
clientId,
{$pull: {
projectIds: projectId,
vaIds: vaId
}}
);
}
// Remove projectId from VAs
if (vaId && Array.isArray(vaId)) {
// await VAModel.updateMany(
// { _id: { $in: vaId } },
// { $pull: {
// projectIds: projectId,
// // 🐞🐞🐞 Here before removing we need to check if VA is still assigned to other projects of the same client
// clientIds: clientId
// }}
// );
for (const va of vaId) {
// Check if VA is still assigned to other projects of the same client
const otherProjectsWithSameClient = await ProjectModel.countDocuments({
_id: { $ne: projectId },
clientId: clientId,
vaAssigned: va
});
// Prepare update operation
const updateOperation = {
$pull: { projectIds: projectId }
};
// Only remove clientId if VA is not assigned to any other projects from this client
if (!otherProjectsWithSameClient) {
updateOperation.$pull = {
...updateOperation.$pull,
clientIds: clientId
};
}
await VAModel.findByIdAndUpdate(va, updateOperation);
}
}
results.projectRemoval = 'Project relationships removed successfully';
}
// Handle VA removal
if (data.va) {
const { vaId, projectId, clientId } = data.va;
if (!vaId) {
throw new Error('VA ID is required for VA removal');
}
// Remove vaId from Projects
if (projectId && Array.isArray(projectId)) {
await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{ $pull: { vaAssigned: vaId } }
);
}
// Remove vaId from Clients
if (clientId && Array.isArray(clientId)) {
await ClientModel.updateMany(
{ _id: { $in: clientId } },
{ $pull: { vaIds: vaId } }
);
}
results.vaRemoval = 'VA relationships removed successfully';
}
// Handle Client removal
if (data.client) {
const { clientId, projectId, vaId } = data.client;
if (!clientId) {
throw new Error('Client ID is required for client removal');
}
// Remove clientId from Projects
if (projectId && Array.isArray(projectId)) {
await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{ clientId: null }
);
}
// Remove clientId from VAs
if (vaId && Array.isArray(vaId)) {
await VAModel.updateMany(
{ _id: { $in: vaId } },
{ $pull: { clientIds: clientId } }
);
}
results.clientRemoval = 'Client relationships removed successfully';
}
return {
success: true,
message: 'Relationships removed successfully',
results
};
} catch (error) {
console.error('Error removing entity relationships:', error);
return {
success: false,
message: error.message,
results: {}
};
}
}
export { updateEntityRelationships, removeEntityRelationships };

View File

@ -1,44 +1,34 @@
import jwt from "jsonwebtoken";
import { VAModel, ClientModel } from "../models/vaModel.js";
import { VAModel } from "../models/vaModel.js";
import logger from "../config/logger.js";
async function authValidator(req, res, next) {
try {
const sf_token = req.headers["x-auth-token"];
if (sf_token) {
const { vaId } = req.params;
if(vaId) req.body.vaId = vaId;
next();
} else {
const accessToken = req.headers.token;
if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" });
const accessToken = req.headers.token;
if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" });
// return res.status(200).json({
// success: true,
// message: "Log Out Successfull",
// });
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
// return res.status(200).json({
// success: true,
// message: "Log Out Successfull",
// });
if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) {
const vaEmail = decoded.email;
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) {
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail })
if (!vaUser) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (!("accessToken" in vaUser) || !vaUser.accessToken) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (vaUser.accessToken === accessToken) {
req.body.vaId = vaUser._id
req.body.vaEmail = vaEmail
next();
} else {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail })
if (!vaUser) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (!("accessToken" in vaUser) || !vaUser.accessToken) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (vaUser.accessToken === accessToken) {
req.body.vaId = vaUser._id
req.body.vaEmail = vaEmail
next();
} else {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
}
@ -48,64 +38,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) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (decoded && decoded.id && decoded.email) {
const clientEmail = decoded.email;
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
if (!clientUser) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (!clientUser.accessToken || clientUser.accessToken !== accessToken) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
// Attach client info to request object for use in subsequent middleware/controllers
req.body.clientId = clientUser._id;
req.body.clientEmail = clientEmail;
req.body.clientName = decoded.name;
next();
} else {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
} catch (error) {
logger.error("clientAuthValidator - Middleware (CATCH Block)", error.message);
if (error.name === "TokenExpiredError") {
return res.status(401).json({ success: false, message: "Token expired" });
}
if (error.name === "JsonWebTokenError") {
return res.status(401).json({ success: false, message: "Invalid token" });
}
return res.status(401).json({ success: false, message: "Authentication failed" });
}
}
export {
authValidator,
clientAuthValidator
authValidator
}

View File

@ -46,9 +46,6 @@ const userDetailsSchema = new Schema({
veemAccount: {
type: String,
},
wiseEmail: {
type: String,
},
phone: {
type: String,
},
@ -89,46 +86,18 @@ const projectSchema = new Schema({
},
vaAssigned: [
{
_id: false,
vaId: {
type: Types.ObjectId,
ref: "VA",
},
isFlexibleAllowed: {
type: Boolean,
default: true
}
}
type: Types.ObjectId,
ref: "VA",
},
],
clientId: {
type: Types.ObjectId,
ref: "Client"
},
dedicatedHours: {
type: Array,
default: ["0", "0", "0", "0", "0", "0", "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'
},
type: Number,
default: 0
}
});
/* ---------------------- Client Details Schema ---------------------- */
@ -136,12 +105,6 @@ const clientDetailsSchema = new Schema({
name: {
type: String,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
email: {
type: String,
required: true,
@ -199,10 +162,8 @@ const clientDetailsSchema = new Schema({
type: String,
},
accountType: {
enum: ["CHECKING", "SAVINGS"],
type: String,
},
accountHolderName: {
type: String,
},
@ -276,14 +237,6 @@ const loggedSchema = new Schema({
projectId: {
type: Types.ObjectId,
ref: "Project"
},
isAdjustment: {
type: Boolean,
default: false
},
adjustmentId: {
type: Types.ObjectId,
ref: "AdjustedHours"
}
})
@ -347,8 +300,14 @@ const vaWorkTrackSchema = new Schema({
ref: "Project",
},
logId: {
type: Types.ObjectId,
ref: "TrackLog",
type: String,
ref: "Logged",
},
title: {
type: String,
},
appName: {
type: String,
},
from: {
type: Date,
@ -362,194 +321,47 @@ 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 });
passwordResetSchema.index({ token: 1, used: 1, expiresAt: 1 });
passwordResetSchema.index({ userType: 1, email: 1 });
const adjustedHoursSchema = new Schema({
vaId: {
type: Types.ObjectId,
ref: "VA",
required: true
},
projectId: {
type: Types.ObjectId,
ref: "Project",
required: true
},
date: {
type: Date,
required: true
},
loginTime: {
type: Date,
required: true
},
logoutTime: {
type: Date,
required: true
},
comment: {
type: String,
required: true
},
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
default: "PENDING"
},
reviewedAt: {
type: Date
},
rejectionReason: {
type: String // If rejected, store the reason
},
adjustedLogs: [{
loginLogId: {
type: Types.ObjectId,
ref: "Logged"
},
logoutLogId: {
type: Types.ObjectId,
ref: "Logged"
}
}]
}, { timestamps: true });
const leavesSchema = new Schema({
vaId: {
type: Types.ObjectId,
ref: "VA",
required: true
},
clientId: {
type: Types.ObjectId,
ref: "Client"
},
dateFrom: {
type: Date,
required: true
},
dateTo: {
type: Date,
required: true
},
days: {
type: String,
required: true
},
comment: {
type: String,
required: true
},
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
leaveType: {
type: String,
enum: ["SICK", "PTO"],
default: "SICK",
required: true
},
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
default: "PENDING"
},
reviewedAt: {
type: Date
},
rejectionReason: {
type: String // If rejected, store the reason
}
}, { 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);
@ -557,8 +369,5 @@ const ProjectModel = model("Project", projectSchema);
const LoggedModel = model("TrackLog", loggedSchema);
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 };

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,51 +1,22 @@
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_clients_callback } from "../../controllers/va/vaClients/vaClients.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";
import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.controller.js";
import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js";
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";
// clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA
clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.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", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC)
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
// 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

@ -9,18 +9,13 @@ import { fileURLToPath } from 'url';
import { dirname } from 'path';
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'));
});
@ -29,56 +24,11 @@ v1Routes.use("/auth_callback", authRoutes);
v1Routes.use("/va", vaRoutes);
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

@ -18,49 +18,27 @@ import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWor
import { va_projects_report_function } from "../../controllers/va/vaProjects/vaProjectReport.controller.js";
import { va_projects_log_list_function } from "../../controllers/va/vaProjects/vaProjectLogList.controller.js";
import { va_request_password_reset } from "../../controllers/passwordReset.controllers.js";
import { va_timesheet_summary_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js";
import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js";
import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js"
import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js"
import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.controller.js";
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js";
import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
import { va_leave_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";
import { va_timesheet_summary_function } from "../../controllers/va/vaLog/vaTimesheetAdjustmentSummary.controller.js";
import { va_timesheet_adjustment_function } from "../../controllers/va/vaLog/vaTimesheetAdjustment.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
vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA
vaRoutes.post("/forgot-password", va_request_password_reset);
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
// 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.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.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC)
// Leaves routes
vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all leaves
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);
// timesheet adjustment routes
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/:projectId/:date", authValidator, va_timesheet_adjustment_function); // Get Timesheet Adjustment for VA on specific project and date
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)
vaRoutes.get("/search", va_filter_search);
@ -68,7 +46,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 +54,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

@ -5,46 +5,29 @@ dotenv.config()
import express from 'express';
import { createServer } from 'http';
import mongoose from "mongoose";
import bodyParser from "body-parser";
import { Server } from 'socket.io';
import { Server } from 'socket.io';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from "node:fs"
import apiRoutes from './routes/index.js'
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
import expressLayouts from "express-ejs-layouts";
import jwt from "jsonwebtoken";
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.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({
@ -63,273 +46,45 @@ app.use(bodyParser.text());
// API Route
app.use('/api', apiRoutes);
app.get("/download-tracker", (req, res) => {
res.render("download_tracker", {
title: 'Download tracker',
version: "3.2.1",
layout: 'layouts/main'
});
app.get('/varegister', (req, res) => {
res.render('va_register', { title: "VA Registration" });
});
app.get("/download-file", (req, res) => {
const os = req.query.os || "windows";
if (os === "windows") {
return res.download("./files/docTracker-3.2.1.exe");
} else {
return res.download("./files/docTracker-3.2.1.dmg");
}
});
app.get('/varegister/:sf_token?', (req, res) => {
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);
// 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'
});
}
});
app.get('/clientregister/:sf_token?', (req, res) => {
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);
// Token valid
return res.render('va_client_register', {
title: 'Client Registration',
email: decoded.email,
layout: 'layouts/vaclientlayout'
});
} catch (error) {
// Invalid / expired token
return res.status(401).render('not_found', {
title: 'Invalid or Expired Link',
layout: 'layouts/error'
});
}
app.get('/clientregister', (req, res) => {
res.render('va_client_register', { title: "Client Registration" });
});
app.get('/auth/password/reset-form', render_reset_form);
app.get('/health', (req, res) => {
res.status(201).send({ status: "Server is healthy" });
})
app.get("/client/login", (req, res) => {
// res.render("client_login")
res.render("client/client_login", {
layout: "layouts/auth", // ⬅️ override layout
title: "DocVA | Login"
});
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
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"
});
} catch (error) {
console.error('Dashboard error:', error);
res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' });
}
});
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', vaData);
} catch (error) {
console.error('Virtual assistants error:', error);
res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' });
}
});
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"
});
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' });
}
});
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" });
}
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 res.status(500).render("not_found", { title: "Server Error" });
}
if (!clientId) {
return res.status(400).render("not_found", { title: "Page not found" });
}
res.render("va_live_tracker", {
title: "VA Live Tracker",
projectId,
vaId,
clientId,
layout: "layouts/valivetrackerlayout" // ✅ ADD THIS
});
});
// 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 })
// })
app.get("/live/:projectId", (req, res) => {
const { projectId } = req.params;
res.render("va_live_tracker", { title: "VA Live Tracker", projectId })
})
// Handling Socket
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
import { ObjectId } from 'mongodb/lib/bson.js';
const vaSocketRespectToClient = new Map();
// Handling Socket
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth?.token;
const projectId = socket.handshake.auth?.projectId;
const userType = socket.handshake.auth?.userType;
const vaId = socket.handshake.auth?.vaId;
console.log("🔐 Received token:", token);
@ -368,7 +123,7 @@ io.use(async (socket, next) => {
console.log(a, b, c, "vaProjClientId vs clientData.clientId");
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}-${projectId.toString()}-va`, socket.id)
vaSocketRespectToClient.set(vaProjClientId.toString(), socket.id)
console.log("✅ Authenticated VA:", vaUser.userDetails.email);
return next();
@ -392,12 +147,9 @@ 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());
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaId.toString()}-${projectId.toString()}-client`, socket.id)
// console.log("✅ Authenticated Client:", clientUser.userDetails.email);
return next();
@ -410,11 +162,13 @@ io.use(async (socket, next) => {
}
});
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,40 +177,28 @@ 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);
}
});
// When VA sends live image
socket.on("sendImage", async ({ token, image, title, projectId }) => {
socket.on("sendImage", async ({ token, image, title }) => {
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
@ -469,45 +211,33 @@ io.on("connection", (socket) => {
return;
}
const project = await ProjectModel.findById(projectId);
if (!project) {
console.log("❌ Invalid Project ID");
return;
}
const clientId = project.clientId.toString();
const vaId = vaUser._id.toString();
const clientVaUniqueId = `${clientId}-${vaId}-${projectId}-client`;
const socketId = vaSocketRespectToClient.get(clientVaUniqueId);
console.log("🟢 Received image 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("receiveImage", image, title);
// }
// }
// Can be like clientId-projectId-vaId to uniquely identify
socket.to(socketId).emit("receiveImage", image, title);
for (let [_, clientSocket] of io.sockets.sockets) {
if (
clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId)
) {
clientSocket.emit("receiveImage", image, title);
}
}
} catch (err) {
console.error("❌ Token verification failed:", err.message);
}
});
socket.on("selectApp", ({ appTitle, clientId, vaId, projectId }) => {
socket.on("selectApp", ({ appTitle, clientId }) => {
console.log(`VA selected app: ${appTitle}`);
console.log(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`), "vaSocketRespectToClient[clientId]")
const sssrrrr = vaSocketRespectToClient.get(clientId);
console.log(vaSocketRespectToClient.get(clientId), sssrrrr, "vaSocketRespectToClient[clientId]")
// You can forward this info to the respective Electron client
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
socket.to(vaSocketRespectToClient.get(clientId)).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);
@ -529,36 +259,7 @@ io.on("connection", (socket) => {
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 (
clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId)
) {
clientSocket.emit("receiveVisibleApps", { apps, vaId });
clientSocket.emit("receiveVisibleApps", apps);
}
}
} catch (err) {
@ -567,78 +268,41 @@ 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"
});
})
// io.on('connection', (socket) => {
app.get("/forgot-password", (req, res) => {
res.render("forgot_password", {
title: "Forgot Password",
layout: "layouts/forgotpasswordlayout"
});
})
// socket.on("sendImage", (base64String) => {
// io.emit("receiveImage", base64String);
// });
app.use((req, res) => {
res.status(404).render("not_found", {
layout: "layouts/error",
title: "404 - Page Not Found"
});
});
// socket.on('disconnect', () => {
// console.log('a user Disconnected');
// });
// });
// Express App Listening
// app.listen(PORT, () => {
// mongoose
// .connect(
// process.env.MONGO_URI,
// )
// .then(() => console.log("mongodb connected"))
// .catch((err) => {
// console.log(err.message);
// process.exit(1);
// });
// console.log(`Express started on http://localhost:${PORT}`);
// });
server.listen(PORT, () => {
mongoose

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: true,
message: "If the email exists, a reset link has been sent"
};
}
@ -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,82 +0,0 @@
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',
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}`);
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)}`);
throw new Error(`Salesforce API error: ${error.response.status} - ${error.response.data.message || 'Unknown error'}`);
} else if (error.request) {
// Request was made but no response received
logger.error('SalesforceService - No response received from Salesforce API');
throw new Error('Salesforce API is unreachable');
} else {
// Something else happened
throw error;
}
}
}
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,
state: userDetails.state || null,
zip: userDetails.zip || null,
billingName: userDetails.billingName || null,
billingAddress: userDetails.billingAddress || null,
billingCity: userDetails.billingCity || null,
billingState: userDetails.billingState || null,
billingZip: userDetails.billingZip || null,
billingEmail: userDetails.billingEmail || null,
position: userDetails.position || null,
signatoryName: userDetails.signatoryName || null,
signatoryEmail: userDetails.signatoryEmail || null,
paymentMethod: userDetails.paymentMethod || null,
bankName: userDetails.bankName || null,
accountNumber: userDetails.accountNumber || null,
routingNumber: userDetails.routingNumber || null,
accountType: userDetails.accountType || null,
accountHolderName: userDetails.accountHolderName || null,
cardNumber: userDetails.cardNumber || null,
expiryDate: userDetails.expiryDate || null,
securityCode: userDetails.securityCode || null,
cardZip: userDetails.cardZip || null,
}
};
}
}
export default SalesforceService;

View File

@ -1,12 +1,8 @@
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) {
this.clientRepository = vaClientRepository;
this.salesforceService = new SalesforceService();
}
async registerClient(data) {
@ -18,14 +14,9 @@ class VaClientService {
}
// Sanitize and prepare data
const clientData = await this.sanitizeClientData(data);
const response = await this.clientRepository.create(clientData);
if(response) {
await this.salesforceService.registerClientInSalesforce(data, response._id);
}
// Send data to Salesforce first
const clientData = this.sanitizeClientData(data);
const response = await this.clientRepository.create(clientData);
return response;
} catch (error) {
logger.error("Client Service - registerClient error: " + error.message);
@ -55,227 +46,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,
@ -289,17 +87,19 @@ class VaClientService {
position: userDetails.position || null,
signatoryName: userDetails.signatoryName || null,
signatoryEmail: userDetails.signatoryEmail || null,
paymentMethod: null,
bankName: null,
accountNumber: null,
routingNumber: null,
accountType: null,
accountHolderName: null,
cardNumber: null,
expiryDate: null,
securityCode: null,
cardZip: null,
}
paymentMethod: userDetails.paymentMethod || null,
bankName: userDetails.bankName || null,
accountNumber: userDetails.accountNumber || null,
routingNumber: userDetails.routingNumber || null,
accountType: userDetails.accountType || null,
accountHolderName: userDetails.accountHolderName || null,
cardNumber: userDetails.cardNumber || null,
expiryDate: userDetails.expiryDate || null,
securityCode: userDetails.securityCode || null,
cardZip: userDetails.cardZip || null,
},
vaIds: payload.vaIds || [],
projectIds: payload.projectIds || [],
};
}
}

View File

@ -29,35 +29,4 @@ async function testPaths() {
}
}
testPaths();// testVpnApi.js
import axios from "axios";
const apiUrl = "http://165.140.240.203:3000/api";
const apiKey = "hellovpn123";
const possiblePaths = [
"/", "/v1", "/servers", "/status", "/vpn", "/health", "/ping",
"/nodes", "/list", "/connect", "/config", "/create", "/createConfig",
"/check", "/organizations", "/users"
];
async function testPaths() {
for (const path of possiblePaths) {
try {
const res = await axios.get(`${apiUrl}${path}`, {
headers: { "x-api-key": apiKey },
timeout: 3000
});
console.log(`✅ ${path} → ${res.status} ${res.statusText}`);
console.log("Response:", res.data);
} catch (err) {
if (err.response) {
console.log(`❌ ${path} → ${err.response.status}`);
} else {
console.log(`⚠️ ${path} → No response`);
}
}
}
}
testPaths();

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

Binary file not shown.

View File

@ -12,17 +12,8 @@
</div>
<div class="mt-4">
<%
let forgotPasswordUrl = `${process.env.FRONTEND_URL}/forgot-password`;
let loginUrl = `${process.env.FRONTEND_URL}/login`;
if (userType === 'client') {
forgotPasswordUrl = '/client/forgot-password';
loginUrl = '/';
}
%>
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
<a href="<%= loginUrl %>" class="btn btn-ghost">Back to Login</a>
<a href="<%= process.env.FRONTEND_URL %>/forgot-password" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
<a href="<%= process.env.FRONTEND_URL %>/login" class="btn btn-ghost">Back to Login</a>
</div>
</div>
</div>

View File

@ -33,13 +33,8 @@
<button type="submit" class="btn btn-primary">Reset Password</button>
</div>
</form>
<%
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
if (userType === 'client') {
backToLoginUrl = '/';
}
%>
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>
<a href="<%= process.env.FRONTEND_URL %>/login" class="small-link">Back to Login</a>
</div>
</div>

View File

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

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 +0,0 @@
<!-- 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>
<script>
const tnyk = "<%= typeof sf_token !== 'undefined' ? sf_token : '' %>";
if (tnyk) {
localStorage.setItem('knoptpq', tnyk);
}
</script>

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 +0,0 @@
<!-- 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>
</div>
</div>
</div>
</div>

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 +0,0 @@
<!-- 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" 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;">
<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>

View File

@ -88,7 +88,7 @@
}
.login-card h2 {
margin-bottom: 15px;
margin-bottom: 25px;
color: var(--color-muted);
font-weight: 500;
font-size: 0.95em;
@ -211,9 +211,10 @@
</head>
<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" />
<!-- <h1>DocVA</h1>
<p>VIRTUAL ASSISTANTS FOR DOCTORS</p> -->
</div>
<div class="login-card">
@ -224,11 +225,11 @@
<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>
<div class="divider">OR</div>
<button type="button" class="btn btn-secondary"># Log in with Email Code</button> -->
<button type="button" class="btn btn-secondary"># Log in with Email Code</button>
</form>
</div>
@ -236,10 +237,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);
}
@ -250,7 +247,6 @@
}
setTheme(getSystemTheme());
checkAuthentication();
document.querySelector("form").addEventListener("submit", async (e) => {
e.preventDefault(); // prevent page reload
@ -272,7 +268,7 @@
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ "userDetails": userDetails }),
body: JSON.stringify({"userDetails": userDetails}),
});
const data = await res.json();
@ -281,13 +277,12 @@
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";
}
if (data.token) {
// 🟢 Save token
localStorage.setItem("authToken", data.token);
// 🟢 Redirect
window.location.href = "/client/dashboard";
} else {
alert("No token received. Please try again.");
}
@ -296,45 +291,7 @@
alert(err.message || "Something went wrong while logging in");
}
});
// Check if user is already authenticated and redirect to dashboard
async function checkAuthentication() {
const authToken = localStorage.getItem("knoptpq");
if (!authToken) {
return false; // No token found
} else {
window.location.href = "/client/dashboard";
return true;
}
// try {
// // Validate the token with the server
// const response = await fetch("/api/v1/client/validate-token", {
// method: "GET",
// headers: {
// "Authorization": `Bearer ${authToken}`,
// "Content-Type": "application/json"
// }
// });
// if (response.ok) {
// // Token is valid, redirect to dashboard
// window.location.href = "/client/dashboard";
// return true;
// } else {
// // Token is invalid, clear localStorage
// localStorage.removeItem("knoptpq");
// localStorage.removeItem("refreshToken");
// return false;
// }
// } catch (error) {
// console.error("Token validation error:", error);
// // If validation fails, clear tokens to be safe
// localStorage.removeItem("knoptpq");
// localStorage.removeItem("refreshToken");
// return false;
// }
}
</script>
</body>
</body>
</html>

View File

@ -1,194 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Download DocTracker</title>
<style>
body {
margin: 0;
font-family: Inter, sans-serif;
display: flex;
justify-content: center;
/* align-items: center; */
height: 100vh;
background: #ffffff;
color: #111;
padding: 20px;
text-align: center;
}
.container {
max-width: 630px;
}
h1 span {
color: #1a168b;
}
.desc {
font-size: 17px;
line-height: 1.6;
color: #333;
margin-bottom: 30px;
}
.os-select {
display: flex;
justify-content: center;
gap: 18px;
margin-bottom: 25px;
}
.os-btn {
padding: 12px 22px;
border-radius: 30px;
border: 2px solid #ddd;
cursor: pointer;
font-size: 15px;
background: white;
display: flex;
align-items: center;
gap: 8px;
transition: 0.2s;
}
.os-btn img {
width: 24px;
height: 24px;
}
.active img {
filter: brightness(0) invert(1);
}
.os-btn.active {
background: #0a0670;
color: #fff;
border-color: #0a0670;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
.download-btn {
margin: 0 auto;
padding: 14px 25px;
background: #d4d7f7;
color: #0a0670;
border: none;
border-radius: 10px;
font-size: 15px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: 0.2s;
}
.download-btn img {
filter: brightness(0) invert(1);
width: 24px;
height: 24px;
}
#downloadBtn {
background: none;
border: none;
font-size: 15px;
color: inherit;
cursor: pointer;
}
.download-btn:hover {
background: #c5c8f0;
}
</style>
</head>
<body>
<div class="container">
<h1>Download <span>DocTracker</span></h1>
<p class="desc">
Let your clients see your real-time work logs, ensuring<br />
you get paid accurately for every minute you've put in.
</p>
<div class="os-select">
<div class="os-btn active" data-os="windows">
<img src="/public/images/windows.png" alt="windows">
<span>Windows</span>
</div>
<div class="os-btn" data-os="mac">
<img src="/public/images/mac-os-logo.png" alt="macos">
<span>MacOS</span>
</div>
</div>
<div class="download-btn">
<!-- <img src="/public/images/downloads.png" alt="macos" > -->
<button id="downloadBtn">
Download Windows version 3.2.1
</button>
</div>
</div>
<script>
const osButtons = document.querySelectorAll(".os-btn");
const downloadBtn = document.getElementById("downloadBtn");
const version = "<%= version || '1.1.1' %>";
let selectedOS = "windows";
// -------- Auto OS Detection --------
const platform = navigator.userAgent.toLowerCase();
if (platform.includes("mac")) selectedOS = "mac";
else selectedOS = "windows";
// Apply pre-selection
osButtons.forEach(btn => {
btn.classList.toggle("active", btn.dataset.os === selectedOS);
});
// Update button text
downloadBtn.innerHTML =
`Download ${selectedOS === 'windows' ? 'Windows' : 'MacOS'} version ${version}`;
// -------- Manual Selection by User --------
osButtons.forEach(btn => {
btn.addEventListener("click", () => {
osButtons.forEach(b => b.classList.remove("active"));
btn.classList.add("active");
selectedOS = btn.dataset.os;
downloadBtn.innerHTML =
`Download ${selectedOS === 'windows' ? 'Windows' : 'MacOS'} version ${version}`;
});
});
// -------- 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 = ``;
});
</script>
</body>
</html>

View File

@ -1,40 +1,132 @@
<!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>
<img id="logo" class="logo" src="/public/images/logo-colored-vertical.svg" alt="DocVA Logo" />
<form class="card">
<h2>Reset your password</h2>
<input type="email" placeholder="Enter your email address*" required />
<button type="submit">Send password reset code</button>
<a href="/client/login">Go back to login</a>
<a href="#">Go back to login</a>
</form>
</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);
}
(function () {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
const logo = document.getElementById("logo");
@ -42,7 +134,7 @@
function applyTheme(isDark) {
const theme = isDark ? "dark" : "light";
document.documentElement.setAttribute("data-theme", theme);
// logo.src = isDark ? "/public/images/logo-white-vertical.svg" : "/public/images/logo-colored-vertical.svg";
logo.src = isDark ? "/public/images/logo-white-vertical.svg" : "/public/images/logo-colored-vertical.svg";
}
// Initial set
@ -62,8 +154,8 @@
e.preventDefault();
const email = emailInput.value.trim();
if (!email || !validateEmail(email)) {
alert("Please enter valid email address.");
if (!email) {
alert("Please enter your email address.");
return;
}
@ -82,14 +174,7 @@
const data = await response.json();
if (response.ok) {
if(response.success) {
if(confirm('Please Check your mail inbox for password reset link')) {
window.location.href = "/client/login"; // Redirect to reset password page
// console.log(response)
}
} else {
alert(data.message || "Failed to send reset link.");
}
window.location.href = "/client/login"; // Redirect to reset password page
} else {
alert("❌ " + (data.message || "Failed to send reset link."));
}
@ -103,3 +188,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>

Some files were not shown because too many files have changed in this diff Show More