Compare commits

...

81 Commits

Author SHA1 Message Date
=bhagwanSingh 4e782e980c a 2025-12-22 15:07:01 +05:30
=bhagwanSingh bfcd64cf0e Generate URL Update 2025-12-22 15:02:11 +05:30
Aakash Mittal bc3b81e12f Added credits tab in client dashboard 2025-12-22 13:56:02 +05:30
Aakash Mittal 41f5eff4d7 Fixed UI issues 2025-12-22 12:12:00 +05:30
Aakash Mittal 4d1abaf9aa merge master brance 2025-12-22 11:33:56 +05:30
=bhagwanSingh 0c14e26937 Added Mac Build Download URL 2025-12-19 18:59:26 +05:30
Aakash Mittal 68a248c25a merge ejs updated layout, fixed api 2025-12-19 18:10:39 +05:30
=bhagwanSingh 221e867a21 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-12-18 19:53:34 +05:30
=bhagwanSingh 39f415b685 a 2025-12-18 19:51:58 +05:30
Aakash Mittal d4cdc94bbd Added company name in client registration 2025-12-18 17:55:11 +05:30
Aakash Mittal 2d61f25d5b merge branch 2025-12-18 17:16:45 +05:30
=bhagwanSingh 99d60185a9 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware - Akash merge fName lName 18 Dec 2025-12-18 16:53:20 +05:30
=bhagwanSingh da3deac57b A 2025-12-18 16:45:08 +05:30
Aakash Mittal 0b0f1d5ab3 merge3 2025-12-18 16:24:26 +05:30
Aakash Mittal b120f75c40 18-11 2025-12-18 16:23:03 +05:30
Aakash Mittal 9f8c76cabd A 2025-12-18 15:49:04 +05:30
Aakash Mittal 5822e67296 Updated client register for first and last name 2025-12-18 15:46:22 +05:30
=bhagwanSingh 412511afec va.routes added authValidator 2025-12-18 14:02:55 +05:30
=bhagwanSingh b3bb9697e8 AuthFetch import add 2025-12-18 14:01:18 +05:30
Aakash Mittal 90438ea937 Fixed time badge calculation in va logs on client dashboard 2025-12-18 11:55:19 +05:30
=bhagwanSingh fd9bf9e517 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-12-17 20:47:15 +05:30
Aakash Mittal 33779a6eb8 Added logs in client dashboard 2025-12-17 20:01:21 +05:30
=bhagwanSingh 44132c351e A 2025-12-17 18:32:19 +05:30
=bhagwanSingh 79baf21819 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware - merge backend f 2025-12-17 18:26:06 +05:30
Aakash Mittal 6cd02b0f85 6:15 2025-12-17 18:15:17 +05:30
Aakash Mittal 344d6181fa revert commit 2025-12-17 16:57:58 +05:30
Aakash Mittal d036a71810 A 2025-12-17 16:53:43 +05:30
=bhagwanSingh a5f8e7bc7f Update getDayNumber 2025-12-17 16:25:54 +05:30
Aakash Mittal 60be1533bc ejs branch merge - 4:14 - 17 Dec 2025-12-17 16:15:53 +05:30
=bhagwanSingh dad88d02bb Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware - 17 Dev - 3:48 2025-12-17 15:49:01 +05:30
Aakash Mittal 46f2d7377f Added days wise dedicated hours in projects 2025-12-17 15:45:59 +05:30
=bhagwanSingh b9f2eb10b4 Auth Fetch Update in Client Register 2025-12-17 14:20:15 +05:30
Aakash Mittal 5f043ed80a Added password option in client registration 2025-12-17 12:27:19 +05:30
=bhagwanSingh efa6ce929e A 2025-12-17 09:29:28 +05:30
=bhagwanSingh b64962738a a 2025-12-17 09:26:04 +05:30
=bhagwanSingh 3d337780da A 2025-12-17 09:24:11 +05:30
=bhagwanSingh 2e5fed545f Auth Fetch and Server Health Update 2025-12-17 09:13:45 +05:30
=bhagwanSingh dba68ed584 Password Rest update 2025-12-16 19:25:41 +05:30
=bhagwanSingh 320d44f4b5 Added authFetch currently getting access toke for all request as health check api is not available on sf ye 2025-12-16 19:01:13 +05:30
=bhagwanSingh 166cf4db45 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-12-16 15:41:57 +05:30
=bhagwanSingh 085e0c0e6d VPN Update 2025-12-16 14:45:30 +05:30
=bhagwanSingh 15bbbd8a98 a 2025-12-16 14:26:17 +05:30
=bhagwanSingh c20db85ada a 2025-12-16 14:21:00 +05:30
=bhagwanSingh 0e8ce1f850 a 2025-12-16 13:42:50 +05:30
=bhagwanSingh 9e4d118009 a 2025-12-16 12:47:57 +05:30
=bhagwanSingh 668c81b86e a 2025-12-16 12:33:20 +05:30
=bhagwanSingh b4b0e27ecf a 2025-12-16 12:21:10 +05:30
=bhagwanSingh c74f792df6 localhost not binding with port 2025-12-16 12:09:46 +05:30
=bhagwanSingh f538371116 Server Keys env update 2025-12-16 11:47:27 +05:30
=bhagwanSingh fd382ccb93 a 2025-12-16 07:29:45 +05:30
Aakash Mittal 287d078b49 Updated track work API 2025-12-15 20:00:02 +05:30
=bhagwanSingh e79383c1cf Update 2025-12-15 18:58:56 +05:30
=bhagwanSingh 67af7df83c Added VPN Disconnect 2025-12-15 16:26:26 +05:30
=bhagwanSingh cbfa03e57c wg0 permission issue 2025-12-15 13:44:59 +05:30
Aakash Mittal 645adea832 Updated track logs api 2025-12-15 13:40:36 +05:30
=bhagwanSingh 03814c6c99 VPN Setup 2025-12-15 12:40:27 +05:30
Aakash Mittal 4ed62b25e8 !5 dec 2025-12-15 11:50:07 +05:30
=bhagwanSingh 2e7731e704 Adding uv Ip on Listener 2025-12-12 14:21:55 +05:30
=bhagwanSingh 9a811e6f00 Password Reset on register 2025-12-12 12:30:40 +05:30
=bhagwanSingh fc821c70ca Test 2025-12-10 20:26:32 +05:30
=bhagwanSingh 6fd3da0837 Test 2025-12-10 20:24:38 +05:30
=bhagwanSingh 140b669bdb Project not showing Fixed 2025-12-10 14:35:53 +05:30
=bhagwanSingh 0ef8d60060 Bug Resolve 2025-12-09 19:15:26 +05:30
=bhagwanSingh bc98edcb4b a 09-Dec 2025-12-09 07:21:19 +05:30
=bhagwanSingh 00cb793a7c Va and project List API update 2025-12-05 13:29:04 +05:30
=bhagwanSingh 59561f211d VaSlip Update 2025-12-04 13:35:12 +05:30
=bhagwanSingh 4d617d3113 Client and VA Update and VA Tip 2025-12-04 11:46:42 +05:30
=bhagwanSingh 15f0f3d782 Socke, Time and leave adjustment update 2025-12-02 18:45:28 +05:30
=bhagwanSingh 7ac50fe720 Manage Timesheet update 2025-12-02 13:20:04 +05:30
=bhagwanSingh b085a88579 ENUM update 2025-12-02 13:01:09 +05:30
=bhagwanSingh 5ee97a7d44 Test auto deploy - Done 2025-12-02 12:32:58 +05:30
=bhagwanSingh c04264f054 Test auto deploy and VPN 2025-12-02 12:32:00 +05:30
=bhagwanSingh a7d770498c Leave API UCment 2025-12-01 17:25:16 +05:30
=bhagwanSingh 2ca5146846 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware - 01-12 2025-12-01 15:49:50 +05:30
=bhagwanSingh 6c8e85336c Test Deploy - Done 2025-12-01 14:04:23 +05:30
=bhagwanSingh 0c578d8121 Test Deploy 2025-12-01 14:02:17 +05:30
=bhagwanSingh d0bb922da1 Server Update 2025-12-01 13:52:55 +05:30
=bhagwanSingh 1baa6da4c7 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware - 30-11 2025-11-30 11:32:37 +05:30
=bhagwanSingh 3154a7e0d3 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware | 28-11 | 16:21 2025-11-28 16:21:40 +05:30
=bhagwanSingh 142f4f77cd 28-11 2025-11-28 16:19:19 +05:30
=bhagwanSingh c7f44c7754 Revert "Client register error - fix"
This reverts commit 384f75f5ef.
2025-11-24 18:56:44 +05:30
76 changed files with 4209 additions and 986 deletions

0
notePad-wg-cmd.txt Normal file
View File

29
package-lock.json generated
View File

@ -41,7 +41,8 @@
"uglify-js": "^3.19.3", "uglify-js": "^3.19.3",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0",
"wireguard-tools": "^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.26.4", "@babel/cli": "^7.26.4",
@ -149,7 +150,6 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0", "@babel/code-frame": "^7.26.0",
@ -2361,7 +2361,6 @@
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@ -2906,7 +2905,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.73",
@ -3744,8 +3742,7 @@
"version": "0.0.1452169", "version": "0.0.1452169",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
"integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause"
"peer": true
}, },
"node_modules/dom-serializer": { "node_modules/dom-serializer": {
"version": "0.1.1", "version": "0.1.1",
@ -6208,6 +6205,12 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"license": "MIT"
},
"node_modules/lodash.once": { "node_modules/lodash.once": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -7996,7 +7999,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -8380,7 +8382,6 @@
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
"integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ip-address": "^9.0.5", "ip-address": "^9.0.5",
"smart-buffer": "^4.2.0" "smart-buffer": "^4.2.0"
@ -9089,7 +9090,6 @@
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
@ -9137,7 +9137,6 @@
"integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@discoveryjs/json-ext": "^0.6.1", "@discoveryjs/json-ext": "^0.6.1",
"@webpack-cli/configtest": "^3.0.1", "@webpack-cli/configtest": "^3.0.1",
@ -9303,7 +9302,6 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "^1.6.0", "@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.2", "@dabh/diagnostics": "^2.0.2",
@ -9353,6 +9351,15 @@
"node": ">= 12.0.0" "node": ">= 12.0.0"
} }
}, },
"node_modules/wireguard-tools": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/wireguard-tools/-/wireguard-tools-0.1.0.tgz",
"integrity": "sha512-oBnnSB6wXGEAB1ZVPfbfpX8WlHNw7wv45r13ILX73felVnrGpMxMjFcKGAsMeRfBOGzXcW0LgAWe+T53dRcI+g==",
"license": "MIT",
"dependencies": {
"lodash.mergewith": "^4.6.2"
}
},
"node_modules/wordwrap": { "node_modules/wordwrap": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",

View File

@ -6,7 +6,7 @@
"scripts": { "scripts": {
"start": "nodemon --inspect src/server.js", "start": "nodemon --inspect src/server.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"deploy": "curl -X POST http://35.162.222.18:9000/hooks/deploy-va-middleware", "deploy": "curl -X POST http://35.95.209.161:9000/hooks/deploy-va-middleware",
"build": "npx webpack --mode production", "build": "npx webpack --mode production",
"dev": "npx webpack --mode development" "dev": "npx webpack --mode development"
}, },
@ -46,7 +46,8 @@
"uglify-js": "^3.19.3", "uglify-js": "^3.19.3",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0",
"wireguard-tools": "^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.26.4", "@babel/cli": "^7.26.4",

View File

@ -165,7 +165,6 @@ body {
} }
.mobile-menu-button { .mobile-menu-button {
display: none;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
align-items: center; align-items: center;
@ -201,6 +200,9 @@ body {
.dashboard-sidebar { .dashboard-sidebar {
display: block; display: block;
} }
.mobile-menu-button {
display: none;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -217,6 +219,10 @@ body {
.dashboard-sidebar.active { .dashboard-sidebar.active {
transform: translateX(0); transform: translateX(0);
} }
.nav-list {
display: none;
}
} }
.sidebar-header { .sidebar-header {
@ -308,7 +314,6 @@ select,
font-size: 14px; font-size: 14px;
} }
[data-theme="dark"] .sidebar-link:hover { [data-theme="dark"] .sidebar-link:hover {
background-color: var(--color-gray-800); background-color: var(--color-gray-800);
} }
@ -626,7 +631,7 @@ select,
.data-table tbody tr { .data-table tbody tr {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
transition: background-color 0.2s; transition: background-color 0.2s;
cursor: pointer; /* cursor: pointer; */
} }
.data-table tbody tr:hover { .data-table tbody tr:hover {
@ -1264,6 +1269,7 @@ tr[data-vaid]:hover {
background: var(--color-gray-400); background: var(--color-gray-400);
} }
@keyframes pulse { @keyframes pulse {
0% { 0% {
opacity: 1; opacity: 1;
@ -1776,7 +1782,6 @@ tr[data-vaid]:hover {
transform: scale(1); transform: scale(1);
} }
/* ================================ /* ================================
PROFILE DROPDOWN (GLASS STYLE) PROFILE DROPDOWN (GLASS STYLE)
================================ */ ================================ */
@ -1930,11 +1935,294 @@ tr[data-vaid]:hover {
color: #dc2626; color: #dc2626;
} }
/* ================================
RESPONSIVE
================================ */
@media (max-width: 768px) { @media (max-width: 768px) {
.profile-name { .profile-name {
display: none; display: none;
} }
} }
/* Accordion Styles */
.accordion {
margin-bottom: 1rem;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: var(--card-bg, #ffffff);
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
background: var(--primary-color, #4f46e5);
color: white;
cursor: pointer;
transition: background 0.3s ease;
}
.accordion-header:hover {
background: var(--primary-dark, #4338ca);
}
.accordion-title {
display: flex;
align-items: center;
gap: 1rem;
font-weight: 600;
font-size: 1.1rem;
text-transform: capitalize;
}
.accordion-icon {
transition: transform 0.3s ease;
}
.accordion.active .accordion-icon {
transform: rotate(180deg);
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion.active .accordion-content {
max-height: 5000px;
}
.table-wrapper {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: var(--bg-secondary, #f8fafc);
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--text-primary, #1e293b);
border-bottom: 2px solid var(--border-color, #e2e8f0);
}
.data-table td {
padding: 1rem;
border-bottom: 1px solid var(--border-color, #e2e8f0);
color: var(--text-primary, #1e293b);
}
.data-table tr:hover {
background: var(--hover-bg, #f1f5f9);
}
.no-data-row td {
text-align: center;
padding: 2rem;
color: var(--text-secondary, #64748b);
}
.time-badge {
background: var(--success-color, #10b981);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
display: inline-block;
text-align: center;
}
.loading-spinner {
text-align: center;
padding: 2rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-color, #e2e8f0);
border-top: 4px solid var(--primary-color, #4f46e5);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-secondary, #64748b);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
color: var(--border-color, #e2e8f0);
}
.empty-state h3 {
margin-bottom: 0.5rem;
color: var(--text-primary, #1e293b);
}
.logs-count {
background: var(--primary-light, #c7d2fe);
color: var(--primary-color, #4f46e5);
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
margin-left: 0.5rem;
}
.payment-option {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
cursor: pointer;
}
.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);
}
.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);
}
.payment-option h4 {
margin-bottom: 8px;
font-size: 16px;
color: var(--text-primary);
}
.payment-option p {
font-size: 14px;
color: var(--text-secondary);
}
.payment-details {
display: none;
margin: 4px 0 16px;
padding: 16px;
border: 1px solid var(--border-color);
border-radius: 8px;
transition: all 0.3s ease;
}
.payment-details.active {
display: block;
animation: slideDown 0.3s ease-out;
}
.form-group {
margin-bottom: 16px;
position: relative;
}
.input-container {
position: relative;
background-color: var(--bg-secondary);
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 {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: var(--text-secondary);
transition: all 0.2s ease;
pointer-events: none;
}
.input-field, .select-field {
width: 100%;
padding: 18px 12px 14px;
border: none;
background: transparent;
outline: none;
font-size: 14px;
border-radius: 6px;
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 {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
border-radius: 6px;
}
[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,
.select-field:valid + .input-label {
top: 2px;
font-size: 12px;
transform: none;
color: var(--accent-light);
}
.data-table #vaCreditTableBody .status{
text-transform: capitalize;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
padding: 4px 10px;
border-radius: 20px;
display: inline-block;
background-color: var(--color-gray-200);
}
.data-table #vaCreditTableBody .status.invoiced {
background-color: rgba(16, 185, 129, 0.1);
color: var(--color-success);
}

View File

@ -670,6 +670,13 @@ body {
background-color: rgba(220, 38, 38, 0.1) !important; 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 */ /* Animations */
@keyframes slideDown { @keyframes slideDown {
from { from {

View File

@ -27,3 +27,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
}); });
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

@ -94,7 +94,7 @@ function createVATableRow(va) {
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-'; const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0; 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>` : '<p class="va-logs va-action" data-va-id="${va.id}" target="_blank">Logs</p>'; 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 ` return `
<tr class="va-row" data-va-id="${va.id}"> <tr class="va-row" data-va-id="${va.id}">

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
// 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();

300
public/js/pages/va-logs.js Normal file
View File

@ -0,0 +1,300 @@
// API base URL
const API_BASE = '/api/v1/client';
function toggleAccordion(accordionId) {
const accordion = document.getElementById(accordionId);
accordion.classList.toggle('active');
// const allAccordions = document.querySelectorAll('.accordion');
// allAccordions.forEach(acc => {
// if (acc.id !== accordionId && acc.classList.contains('active')) {
// acc.classList.remove('active');
// }
// });
}
function formatTime(milliseconds) {
if (!milliseconds) return '0m';
const seconds = Math.floor(milliseconds / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m ${seconds % 60}s`;
}
return `${minutes}m ${seconds % 60}s`;
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
function formatTimeOnly(dateString) {
const date = new Date(dateString);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function calculateTotalIndividualLogsHours(workTrackLogs) {
let total = 0;
workTrackLogs.forEach(workTrack => {
if (workTrack.logs && Array.isArray(workTrack.logs)) {
workTrack.logs.forEach(log => {
total += log.totalTime || 0;
});
}
});
return total;
}
function countIndividualLogs(workTrackLogs) {
let count = 0;
workTrackLogs.forEach(workTrack => {
if (workTrack.logs && Array.isArray(workTrack.logs)) {
count += workTrack.logs.length;
}
});
return count;
}
function calculateTotalHours(logs) {
return logs.reduce((total, log) => total + (log.totalTime || 0), 0);
}
// Create project accordion
function createProjectAccordion(projectData, index) {
const workTrackLogs = projectData.workTrackLogs || [];
const totalHours = calculateTotalIndividualLogsHours(workTrackLogs);
const individualLogsCount = countIndividualLogs(workTrackLogs);
const projectName = projectData.projectDetails?.name || `Project ${index + 1}`;
const accordionId = `project-${index}`;
return `
<div class="accordion" id="${accordionId}">
<div class="accordion-header" onclick="toggleAccordion('${accordionId}')">
<div class="accordion-title">
${projectName}
<span class="logs-count">${individualLogsCount} activities</span>
</div>
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="text-align: right;">
<div class="time-badge">${formatTime(totalHours)}</div>
</div>
<i class="fas fa-chevron-down accordion-icon"></i>
</div>
</div>
<div class="accordion-content">
${individualLogsCount > 0 ? createIndividualLogsTable(workTrackLogs) : createNoLogsMessage()}
</div>
</div>
`;
}
// Create table for individual log entries
function createIndividualLogsTable(workTrackLogs) {
// Collect all individual logs from all work tracks
const allIndividualLogs = [];
workTrackLogs.forEach(workTrack => {
if (workTrack.logs && Array.isArray(workTrack.logs)) {
workTrack.logs.forEach(log => {
allIndividualLogs.push(log);
});
}
});
return `
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Title</th>
<th>App Name</th>
<th>From</th>
<th>To</th>
<th>Total Time</th>
</tr>
</thead>
<tbody>
${allIndividualLogs.map((log, index) => createIndividualLogRow(log, index)).join('')}
</tbody>
</table>
</div>
`;
}
// Create a single row for individual log entry
function createIndividualLogRow(log, index) {
const title = log.title || `Activity ${index + 1}`;
const appName = log.appName || 'Unknown App';
// Parse dates
const fromDate = log.from;
const toDate = log.to || log.from;
// Calculate duration
let duration = log.totalTime || 0;
if (!duration && fromDate && toDate) {
const from = new Date(fromDate);
const to = new Date(toDate);
duration = Math.floor((to - from) / 1000);
}
return `
<tr>
<td>
<strong>${title}</strong>
</td>
<td>${appName}</td>
<td>
<div class="datetime-cell">
<div class="date-text">${formatDate(fromDate)}</div>
<div class="time-text">${formatTimeOnly(fromDate)}</div>
</div>
</td>
<td>
<div class="datetime-cell">
<div class="date-text">${formatDate(toDate)}</div>
<div class="time-text">${formatTimeOnly(toDate)}</div>
</div>
</td>
<td>
<span class="time-badge">${formatTime(duration)}</span>
</td>
</tr>
`;
}
// Create no logs message
function createNoLogsMessage() {
return `
<div style="text-align: center; padding: 2rem; color: #64748b;">
<i class="fas fa-clipboard"></i>
<h4>No Activity Logs</h4>
<p>No detailed activity logs available for this project.</p>
</div>
`;
}
// Load VA logs
async function loadVALogs() {
const loadingState = document.getElementById('loadingState');
const emptyState = document.getElementById('emptyState');
const projectsContainer = document.getElementById('projectsContainer');
try {
// Get VA ID from URL
const urlParams = new URLSearchParams(window.location.search);
const vaId = urlParams.get('va');
if (!vaId) {
window.location.href = '/client/virtual-assistants';
return;
}
// Fetch VA logs data
const response = await fetchWithAuth(`${API_BASE}/vaLogsData/${vaId}`);
if (!response || !response.ok) {
throw new Error('Failed to load VA logs');
}
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Failed to load VA logs');
}
// Hide loading state
loadingState.style.display = 'none';
const projectLogs = data.data?.projectLogs || [];
// Handle empty state
if (projectLogs.length === 0) {
emptyState.style.display = 'block';
projectsContainer.style.display = 'none';
return;
}
// Show projects container
projectsContainer.style.display = 'block';
emptyState.style.display = 'none';
// Create accordions for each project
let accordionsHTML = '';
projectLogs.forEach((project, index) => {
accordionsHTML += createProjectAccordion(project, index);
});
projectsContainer.innerHTML = accordionsHTML;
// Auto-open first accordion
if (projectLogs.length > 0) {
setTimeout(() => {
const firstAccordion = document.getElementById('project-0');
if (firstAccordion) {
firstAccordion.classList.add('active');
}
}, 100);
}
} catch (error) {
console.error("Error loading VA logs:", error);
loadingState.style.display = 'none';
projectsContainer.style.display = 'none';
// Show error in empty state
emptyState.innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
<h3>Error Loading Data</h3>
<p>${error.message}</p>
<button onclick="loadVALogs()" class="btn btn-primary" style="margin-top: 1rem;">
<i class="fas fa-redo"></i> Retry
</button>
`;
emptyState.style.display = 'block';
}
}
document.addEventListener('DOMContentLoaded', function () {
// Mobile menu toggle
const menuToggle = document.querySelector('.mobile-menu-button');
const sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
});
// Close modal with escape key
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
closeLogoutModal();
}
});
document.getElementById('logoutModal').addEventListener('click', function (e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
// Load VA logs on page load
loadVALogs();
});

View File

@ -491,10 +491,13 @@ async function validateStep(step) {
} }
} else if (step === 1) { } else if (step === 1) {
const email = document.getElementById('email').value; const email = document.getElementById('email').value;
const companyName = document.getElementById('company-name').value; const firstName = document.getElementById('first-name').value;
if (!validateEmail(email) && !companyName) { const lastName = document.getElementById('last-name').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!firstName && !lastName && !password && !confirmPassword) {
isValid = false; isValid = false;
showError('Both Fields are Required', null, step); showError('Fields are Required', null, step);
let requiredFields1 = form.querySelectorAll('[required]'); let requiredFields1 = form.querySelectorAll('[required]');
requiredFields1.forEach(field => { requiredFields1.forEach(field => {
if (!field.value.trim()) { if (!field.value.trim()) {
@ -516,9 +519,48 @@ async function validateStep(step) {
isValid = false; isValid = false;
} }
} }
if (!firstName) {
isValid = false;
showError('First Name is Required', 'first-name', step);
return isValid;
}
if (!lastName) {
isValid = false;
showError('Last Name is Required', 'last-name', step);
return isValid;
}
if (password.length < 8) {
showError('Password must be at least 8 characters long', 'password', step);
isValid = false;
}
if (password !== confirmPassword) {
document.getElementById('password-match-error').style.display = 'block';
document.getElementById('confirmPassword').classList.add('field-error');
showError('Passwords do not match', 'confirmPassword', step);
isValid = false;
} else {
document.getElementById('password-match-error').style.display = 'none';
document.getElementById('confirmPassword').classList.remove('field-error');
}
// Validate other required fields
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => {
if (field.id !== 'email' && !field.value.trim()) {
isValid = false;
field.classList.add('field-error');
} else if (field.id !== 'email') {
field.classList.remove('field-error');
}
});
} else if (step === 2) {
const companyName = document.getElementById('company-name').value;
if (!companyName) { if (!companyName) {
isValid = false; isValid = false;
showError('Company / Client Name is Required', null, step); showError('Company Name is Required', 'company-name', step);
return isValid; return isValid;
} }
@ -610,10 +652,13 @@ function getFormId(step) {
function saveStepData(step) { function saveStepData(step) {
switch(step) { switch(step) {
case 1: case 1:
formData.userDetails.name = document.getElementById('company-name').value; formData.userDetails.firstName = document.getElementById('first-name').value;
formData.userDetails.lastName = document.getElementById('last-name').value;
formData.userDetails.email = document.getElementById('email').value; formData.userDetails.email = document.getElementById('email').value;
formData.userDetails.password = document.getElementById('password').value;
break; break;
case 2: case 2:
formData.userDetails.name = document.getElementById('company-name').value;
formData.userDetails.address = document.getElementById('address').value; formData.userDetails.address = document.getElementById('address').value;
formData.userDetails.city = document.getElementById('city').value; formData.userDetails.city = document.getElementById('city').value;
formData.userDetails.state = document.getElementById('state').value; formData.userDetails.state = document.getElementById('state').value;
@ -767,7 +812,7 @@ async function submitForm() {
nextStep(6); nextStep(6);
setTimeout(() => { setTimeout(() => {
window.location.href = '/client/login'; window.location.href = '/';
}, 1000); }, 1000);
} else { } else {
// Handle server errors // Handle server errors
@ -857,28 +902,29 @@ async function detectLocation() {
await populateStates(countryCode, 'state'); await populateStates(countryCode, 'state');
await populateStates(countryCode, 'billing-state'); await populateStates(countryCode, 'billing-state');
const stateSelect = document.getElementById('state'); // auto populate state if region matches
const billingStateSelect = document.getElementById('billing-state'); // const stateSelect = document.getElementById('state');
// const billingStateSelect = document.getElementById('billing-state');
if (stateSelect && region) { // if (stateSelect && region) {
const matchedState = Array.from(stateSelect.options).find( // const matchedState = Array.from(stateSelect.options).find(
opt => opt.text.toLowerCase() === region.toLowerCase() // opt => opt.text.toLowerCase() === region.toLowerCase()
); // );
if (matchedState) { // if (matchedState) {
stateSelect.value = matchedState.value; // stateSelect.value = matchedState.value;
stateSelect.dispatchEvent(new Event('change')); // stateSelect.dispatchEvent(new Event('change'));
} // }
} // }
if (billingStateSelect && region) { // if (billingStateSelect && region) {
const matchedBillingState = Array.from(billingStateSelect.options).find( // const matchedBillingState = Array.from(billingStateSelect.options).find(
opt => opt.text.toLowerCase() === region.toLowerCase() // opt => opt.text.toLowerCase() === region.toLowerCase()
); // );
if (matchedBillingState) { // if (matchedBillingState) {
billingStateSelect.value = matchedBillingState.value; // billingStateSelect.value = matchedBillingState.value;
billingStateSelect.dispatchEvent(new Event('change')); // billingStateSelect.dispatchEvent(new Event('change'));
} // }
} // }
} catch (error) { } catch (error) {
console.error('Error detecting location:', error); console.error('Error detecting location:', error);
@ -1240,7 +1286,181 @@ function preventNumbers(event) {
return false; return false;
} }
// Function to validate company/client name // Function to validate first name
function validateFirstName(input) {
const errorElement = document.getElementById('first-name-error');
const value = input.value;
// Check various invalid patterns
const validationResult = validateFirstNameContent(value);
if (!validationResult.isValid && value.trim().length > 0) {
errorElement.textContent = validationResult.message;
errorElement.style.display = 'block';
input.classList.add('field-error');
return false;
} else {
errorElement.style.display = 'none';
input.classList.remove('field-error');
return true;
}
}
function validateFirstNameContent(firstName) {
const trimmed = firstName.trim();
if (trimmed.length === 0) {
return { isValid: false, message: 'First name is required' };
}
// Check minimum length
if (trimmed.length < 2) {
return { isValid: false, message: 'First name must be at least 2 characters' };
}
// Remove whitespace for content analysis
const content = trimmed.replace(/\s/g, '');
// Check if contains only special characters
if (/^[^a-zA-Z0-9]+$/.test(content)) {
return { isValid: false, message: 'First name cannot be only special characters' };
}
// Check if contains at least some valid content (letters or numbers)
if (!/[a-zA-Z0-9]/.test(content)) {
return { isValid: false, message: 'First name must contain letters or numbers' };
}
// Optional: Check for excessive special characters (more than 50%)
const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length;
if (specialCharCount > content.length / 2) {
return { isValid: false, message: 'First name contains too many special characters' };
}
return { isValid: true, message: '' };
}
// Function to prevent invalid character input in real-time
function preventInvalidFirstNameChars(event) {
const charCode = event.which ? event.which : event.keyCode;
const key = event.key;
// Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas
// Also allow common editing keys
if ((charCode >= 65 && charCode <= 90) || // A-Z
(charCode >= 97 && charCode <= 122) || // a-z
(charCode >= 48 && charCode <= 57) || // 0-9
charCode === 32 || // space
charCode === 45 || // hyphen
charCode === 39 || // apostrophe
charCode === 38 || // ampersand (&)
charCode === 46 || // period (.)
charCode === 44 || // comma (,)
charCode === 8 || // backspace
charCode === 9 || // tab
charCode === 37 || // left arrow
charCode === 39 || // right arrow
charCode === 16 || // shift
charCode === 17 || // ctrl
charCode === 18 || // alt
charCode === 91 || // cmd (Mac)
/^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters
return true;
}
// Prevent invalid characters
event.preventDefault();
return false;
}
// Function to validate last name
function validateLastName(input) {
const errorElement = document.getElementById('last-name-error');
const value = input.value;
// Check various invalid patterns
const validationResult = validateLastNameContent(value);
if (!validationResult.isValid && value.trim().length > 0) {
errorElement.textContent = validationResult.message;
errorElement.style.display = 'block';
input.classList.add('field-error');
return false;
} else {
errorElement.style.display = 'none';
input.classList.remove('field-error');
return true;
}
}
function validateLastNameContent(lastName) {
const trimmed = lastName.trim();
if (trimmed.length === 0) {
return { isValid: false, message: 'Last name is required' };
}
// Check minimum length
if (trimmed.length < 2) {
return { isValid: false, message: 'Last name must be at least 2 characters' };
}
// Remove whitespace for content analysis
const content = trimmed.replace(/\s/g, '');
// Check if contains only special characters
if (/^[^a-zA-Z0-9]+$/.test(content)) {
return { isValid: false, message: 'Last name cannot be only special characters' };
}
// Check if contains at least some valid content (letters or numbers)
if (!/[a-zA-Z0-9]/.test(content)) {
return { isValid: false, message: 'Last name must contain letters or numbers' };
}
// Optional: Check for excessive special characters (more than 50%)
const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length;
if (specialCharCount > content.length / 2) {
return { isValid: false, message: 'Last name contains too many special characters' };
}
return { isValid: true, message: '' };
}
// Function to prevent invalid character input in real-time
function preventInvalidLastNameChars(event) {
const charCode = event.which ? event.which : event.keyCode;
const key = event.key;
// Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas
// Also allow common editing keys
if ((charCode >= 65 && charCode <= 90) || // A-Z
(charCode >= 97 && charCode <= 122) || // a-z
(charCode >= 48 && charCode <= 57) || // 0-9
charCode === 32 || // space
charCode === 45 || // hyphen
charCode === 39 || // apostrophe
charCode === 38 || // ampersand (&)
charCode === 46 || // period (.)
charCode === 44 || // comma (,)
charCode === 8 || // backspace
charCode === 9 || // tab
charCode === 37 || // left arrow
charCode === 39 || // right arrow
charCode === 16 || // shift
charCode === 17 || // ctrl
charCode === 18 || // alt
charCode === 91 || // cmd (Mac)
/^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters
return true;
}
// Prevent invalid characters
event.preventDefault();
return false;
}
// Function to validate company name
function validateCompanyName(input) { function validateCompanyName(input) {
const errorElement = document.getElementById('company-name-error'); const errorElement = document.getElementById('company-name-error');
const value = input.value; const value = input.value;
@ -1295,7 +1515,7 @@ function validateCompanyNameContent(companyName) {
} }
// Function to prevent invalid character input in real-time // Function to prevent invalid character input in real-time
function preventInvalidCompanyChars(event) { function preventInvalidCompanyNameChars(event) {
const charCode = event.which ? event.which : event.keyCode; const charCode = event.which ? event.which : event.keyCode;
const key = event.key; const key = event.key;
@ -1955,3 +2175,51 @@ function preventInvalidAccountHolderChars(event) {
event.preventDefault(); event.preventDefault();
return false; return false;
} }
// Function to generate a strong password
function generatePassword() {
const length = 12;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
let password = "";
// Ensure at least one of each required character type
password += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)];
password += "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)];
password += "0123456789"[Math.floor(Math.random() * 10)];
password += "!@#$%^&*"[Math.floor(Math.random() * 8)];
// Fill the rest randomly
for (let i = password.length; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
// Shuffle the password
password = password.split('').sort(() => 0.5 - Math.random()).join('');
// Set the password field
document.getElementById('password').value = password;
document.getElementById('confirmPassword').value = password;
// Trigger input events to update UI
document.getElementById('password').dispatchEvent(new Event('input'));
document.getElementById('confirmPassword').dispatchEvent(new Event('input'));
}
// Function to toggle password visibility
function togglePasswordVisibility() {
const passwordField = document.getElementById('password');
const confirmField = document.getElementById('confirmPassword');
const toggleButton = document.querySelector('.password-toggle');
if (passwordField.type === 'password') {
passwordField.type = 'text';
confirmField.type = 'text';
toggleButton.textContent = 'Hide Password';
} else {
passwordField.type = 'password';
confirmField.type = 'password';
toggleButton.textContent = 'Show Password';
}
}

View File

@ -617,16 +617,22 @@
} else { } else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') { if (paymentMethod === 'payoneer') {
// const payoneerAccount = document.getElementById('payoneerAccount').value; const payoneerAccount = document.getElementById('payoneerAccount').value;
// if (!payoneerAccount) { if (!payoneerAccount) {
// showError('Please enter your Payoneer account number', 'payoneerAccount', step); showError('Please enter your Payoneer account number', 'payoneerAccount', step);
// isValid = false; isValid = false;
// } } else if (!validateEmail(payoneerAccount)) {
showError('Please enter a valid email address for Payoneer', 'payoneerAccount', step);
isValid = false;
}
} else if (paymentMethod === 'veem') { } else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value; const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) { if (!veemAccount) {
showError('Please enter your Veem account number', 'veemAccount', step); showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false; isValid = false;
} else if (!validateEmail(veemAccount)) {
showError('Please enter a valid email address for Veem', 'veemAccount', step);
isValid = false;
} }
} else if (paymentMethod === 'wise') { } else if (paymentMethod === 'wise') {
const wiseEmail = document.getElementById('wiseEmail').value; const wiseEmail = document.getElementById('wiseEmail').value;
@ -703,9 +709,9 @@
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
formData.userDetails.paymentMethod = paymentMethod; formData.userDetails.paymentMethod = paymentMethod;
// if (document.getElementById('payoneerAccount').value) { if (document.getElementById('payoneerAccount').value) {
// formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value; formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
// } }
if (document.getElementById('veemAccount').value) { if (document.getElementById('veemAccount').value) {
formData.userDetails.veemAccount = document.getElementById('veemAccount').value; formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
} }
@ -734,7 +740,7 @@
// Show appropriate payment details // Show appropriate payment details
if (method === 'payoneer') { if (method === 'payoneer') {
// document.getElementById('payoneer-details').style.display = 'block'; document.getElementById('payoneer-details').style.display = 'block';
} else if (method === 'veem') { } else if (method === 'veem') {
document.getElementById('veem-details').style.display = 'block'; document.getElementById('veem-details').style.display = 'block';
} else if (method === 'wise') { } else if (method === 'wise') {
@ -763,6 +769,48 @@
return true; return true;
} }
function validateVeemEmail(input) {
const wiseError = document.getElementById('veem-error');
const value = input.value.trim();
// Remove any validation styling first
input.style.border = '';
wiseError.style.display = 'none';
if (value.length > 0) {
// Check if it's a valid email
if (!validateEmail(value)) {
wiseError.textContent = 'Please enter a valid email address for Veem';
wiseError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
}
}
return true;
}
function validatePayoneerEmail(input) {
const wiseError = document.getElementById('payoneer-error');
const value = input.value.trim();
// Remove any validation styling first
input.style.border = '';
wiseError.style.display = 'none';
if (value.length > 0) {
// Check if it's a valid email
if (!validateEmail(value)) {
wiseError.textContent = 'Please enter a valid email address for Payoneer';
wiseError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
}
}
return true;
}
// Function to generate a strong password // Function to generate a strong password
function generatePassword() { function generatePassword() {
const length = 12; const length = 12;

View File

@ -1,5 +1,6 @@
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js"; import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import getDayNumber from "../../../utils/getDayNumber.js";
// Getting client dashboard data - all VAs, projects, and active VAs // Getting client dashboard data - all VAs, projects, and active VAs
async function client_dashboard(req, res) { async function client_dashboard(req, res) {
@ -314,9 +315,11 @@ async function client_dashboard(req, res) {
}; };
}); });
const dayNumber = getDayNumber();
// Calculate dashboard statistics // Calculate dashboard statistics
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0); const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0); const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours[dayNumber] || 0), 0);
const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive); const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive);
const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive); const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive);

View File

@ -0,0 +1,40 @@
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

@ -0,0 +1,117 @@
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,5 +1,6 @@
import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js"; import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import getDayNumber from "../../../utils/getDayNumber.js";
function formatHoursToReadable(hours) { function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours); const wholeHours = Math.floor(hours);
@ -34,8 +35,6 @@ async function client_dashboard_projects(req, res) {
// STEP 1: Get client ID from token // STEP 1: Get client ID from token
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail }); const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
console.log("Line 38" , clientUser)
if (!clientUser) { if (!clientUser) {
return res.status(404).json({ success: false, message: "Client not found" }); return res.status(404).json({ success: false, message: "Client not found" });
} }
@ -114,13 +113,15 @@ async function client_dashboard_projects(req, res) {
const validVADetails = assignedVADetails.filter(Boolean); const validVADetails = assignedVADetails.filter(Boolean);
const activeVAsCount = validVADetails.filter(va => va.isActive).length; const activeVAsCount = validVADetails.filter(va => va.isActive).length;
const dayNumber = getDayNumber();
return { return {
id: project._id, id: project._id,
name: project.name, name: project.name,
hourlyRate: project.hourlyRate, hourlyRate: project.hourlyRate,
vaAssigned: validVADetails.length, vaAssigned: validVADetails.length,
activeVAs: activeVAsCount, activeVAs: activeVAsCount,
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || "0h", dedicatedHours: formatHoursToReadable(project.dedicatedHours[dayNumber]) || "0h",
status: project.status || '', status: project.status || '',
assignedVADetails: validVADetails assignedVADetails: validVADetails
}; };

View File

@ -2,9 +2,22 @@ import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.js"; import VaClientService from "../../services/vaClient.service.js";
import logger from "../../config/logger.js"; import logger from "../../config/logger.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import PasswordResetService from "../../services/passwordReset.service.js";
import PasswordResetRepository from "../../repository/passwordReset.repository.js";
const vaClientService = new VaClientService(new VaClientRepository()); const vaClientService = new VaClientService(new VaClientRepository());
const passwordResetService = new PasswordResetService(
new PasswordResetRepository(),
// new EmailService({
// host: process.env.SMTP_HOST,
// port: process.env.SMTP_PORT,
// user: process.env.SMTP_USER,
// pass: process.env.SMTP_PASS,
// from: process.env.EMAIL_FROM,
// })
);
async function va_register_client(req, res) { async function va_register_client(req, res) {
logger.info("ClientController - Register Client Hit ..."); logger.info("ClientController - Register Client Hit ...");
@ -12,16 +25,19 @@ async function va_register_client(req, res) {
try { try {
const payload = req.body || {}; const payload = req.body || {};
const { userDetails } = payload; const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.name) { if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.lastName || !userDetails.password || !userDetails.name) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Missing required fields: name and email are required" message: "Missing required fields: firstName, lastName, email, password, and company name are required"
}); });
} }
const created = await vaClientService.registerClient(payload); const created = await vaClientService.registerClient(payload);
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`); logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
await passwordResetService.requestPasswordReset(userDetails.email, 'client');
logger.info(`Client password reset requested for: ${userDetails.email}`);
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: created, data: created,
@ -175,7 +191,7 @@ async function update_client(req, res) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Internal Server Error" message: error.message
}); });
} }
} }
@ -284,8 +300,8 @@ async function update_client_password(req, res) {
} }
if (error.message.includes("Current password is incorrect") || if (error.message.includes("Current password is incorrect") ||
error.message.includes("New passwords do not match") || error.message.includes("New passwords do not match") ||
error.message.includes("All password fields are required")) { error.message.includes("All password fields are required")) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: error.message message: error.message

View File

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

View File

@ -56,7 +56,7 @@ async function delete_project_sf(req, res) {
// 2. Remove project from all assigned VAs' projectIds arrays // 2. Remove project from all assigned VAs' projectIds arrays
if (vaAssigned.length > 0) { if (vaAssigned.length > 0) {
const vaResult = await VAModel.updateMany( const vaResult = await VAModel.updateMany(
{ _id: { $in: vaAssigned } }, { _id: { $in: vaAssigned.vaId } },
{ $pull: { projectIds: projectObjectId } } { $pull: { projectIds: projectObjectId } }
); );
deletionSummary.vasUpdated = vaResult.modifiedCount; deletionSummary.vasUpdated = vaResult.modifiedCount;

View File

@ -4,8 +4,8 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
// Register a new VA // Register a new VA
async function project_add_sf(req, res) { async function project_add_sf(req, res) {
try { try {
const payload = req.body|| {}; const payload = req.body || {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload; const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
// Validate required fields // Validate required fields
if (!name) { if (!name) {
@ -34,46 +34,55 @@ async function project_add_sf(req, res) {
} }
// Validate and convert vaAssigned to array of ObjectIds // Validate and convert vaAssigned to array of ObjectIds
let vaAssignedArray = []; // let vaAssignedArray = [];
if (vaAssigned) { // if (vaAssigned) {
if (Array.isArray(vaAssigned)) { // if (Array.isArray(vaAssigned)) {
try { // try {
vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId)); // vaAssignedArray = vaAssigned.map(vaId => {
} catch (error) { // if(typeof vaId === 'object' && "isFlexibleAllowed" in vaId){
return res.status(400).json({ // // Handle Error form SF Side for now - That vaId is coming as object
success: false, // return new Types.ObjectId(vaId.vaId)
message: "Invalid VA ID format in vaAssigned array" // } else {
}); // return new Types.ObjectId(vaId)
} // }
} else if (typeof vaAssigned === 'string') { // });
// Handle single VA ID as string // } catch (error) {
try { // return res.status(400).json({
vaAssignedArray = [new Types.ObjectId(vaAssigned)]; // success: false,
} catch (error) { // message: "Invalid VA ID format in vaAssigned array"
return res.status(400).json({ // });
success: false, // }
message: "Invalid VA ID format" // } 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 // sanitize/construct allowed fields
const toCreate = { const toCreate = {
name: name.trim(), name: name.trim(),
hourlyRate: hourlyRate || '0', hourlyRate: hourlyRate || '0',
clientId: clientObjectId, clientId: clientObjectId,
vaAssigned: vaAssignedArray, vaAssigned: vaAssigned,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0, dedicatedHours: dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"],
vaWorkingScheduleStart: vaWorkingScheduleStart || null, vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null, vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowedPt: isFlexibleAllowedPt || true, isFlexibleAllowed: isFlexibleAllowed || true,
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null, vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
}; };
const created = await ProjectModel.create(toCreate); const created = await ProjectModel.create(toCreate);
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
const relationshipUpdate = await updateEntityRelationships({ const relationshipUpdate = await updateEntityRelationships({
project: { project: {
projectId: created._id, projectId: created._id,

View File

@ -16,10 +16,10 @@ async function update_project_sf(req, res) {
vaAssigned, vaAssigned,
dedicatedHours, dedicatedHours,
status, status,
totalLoggedHours , totalLoggedHours,
vaWorkingScheduleStart, vaWorkingScheduleStart,
vaWorkingScheduleEnd, vaWorkingScheduleEnd,
isFlexibleAllowedPt, isFlexibleAllowed,
vaWorkingDelayAllowedStart, vaWorkingDelayAllowedStart,
vaWorkingDelayAllowedEnd vaWorkingDelayAllowedEnd
} = payload; } = payload;
@ -74,7 +74,7 @@ async function update_project_sf(req, res) {
// Update dedicated hours if provided // Update dedicated hours if provided
if (dedicatedHours !== undefined) { if (dedicatedHours !== undefined) {
updateData.dedicatedHours = parseFloat(dedicatedHours) || 0; updateData.dedicatedHours = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"];
} }
// Update total logged hours if provided // Update total logged hours if provided
@ -102,8 +102,8 @@ async function update_project_sf(req, res) {
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null; updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
} }
if (isFlexibleAllowedPt !== undefined) { if (isFlexibleAllowed !== undefined) {
updateData.isFlexibleAllowedPt = isFlexibleAllowedPt; updateData.isFlexibleAllowed = isFlexibleAllowed;
} }
if (vaWorkingDelayAllowedStart !== undefined) { if (vaWorkingDelayAllowedStart !== undefined) {
@ -114,15 +114,26 @@ async function update_project_sf(req, res) {
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null; updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
} }
if (vaAssigned !== undefined && vaAssigned !== null && Array.isArray(vaAssigned)) {
updateData.vaAssigned = vaAssigned;
}
// Handle VA assignments - this requires relationship updates // Handle VA assignments - this requires relationship updates
if (vaAssigned !== undefined) { if (vaAssigned !== undefined) {
let newVaAssignedArray = []; let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
// Parse and validate VA IDs // Parse and validate VA IDs
if (vaAssigned && vaAssigned.length > 0) { if (vaAssigned && vaAssigned.length > 0) {
if (Array.isArray(vaAssigned)) { if (Array.isArray(vaAssigned)) {
try { try {
newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId)); 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) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@ -141,14 +152,14 @@ async function update_project_sf(req, res) {
} }
} }
updateData.vaAssigned = newVaAssignedArray; // updateData.vaAssigned = newVaAssignedArray;
// Calculate VAs to add and remove for relationship updates // Calculate VAs to add and remove for relationship updates
const currentVaIds = existingProject.vaAssigned.map(va => va.toString()); const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.toString());
const newVaIds = newVaAssignedArray.map(va => va.toString()); const newVaIds = newVaAssignedArray.map(va => va.toString());
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString())); const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString())); const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.vaId.toString()));
// Update relationships for added VAs // Update relationships for added VAs
if (vaToAdd.length > 0) { if (vaToAdd.length > 0) {
@ -187,7 +198,7 @@ async function update_project_sf(req, res) {
updateData, updateData,
{ new: true, runValidators: true } { new: true, runValidators: true }
).populate('clientId', 'userDetails.name userDetails.email') ).populate('clientId', 'userDetails.name userDetails.email')
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email'); .populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
// If VA assignments were changed but relationship update failed, log warning // If VA assignments were changed but relationship update failed, log warning
if (vaAssigned !== undefined) { if (vaAssigned !== undefined) {

View File

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

View File

@ -1,5 +1,6 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js"; import { VAModel, LeavesModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Apply for leave // Apply for leave
async function va_leave_apply_function(req, res) { async function va_leave_apply_function(req, res) {
@ -158,13 +159,13 @@ async function va_leave_apply_function(req, res) {
"dateTo": endDate.toISOString().split('T')[0], "dateTo": endDate.toISOString().split('T')[0],
"totalDays": totalDays, "totalDays": totalDays,
"leaveType": leaveType, "leaveType": leaveType,
"comment": processedComment.plainText, // Send plain text to Salesforce "comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
"attachments": processedComment.extractedImages, // Send extracted images info "attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING", "status": "PENDING",
"rejectionReason": "" "rejectionReason": ""
} }
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -176,6 +177,7 @@ async function va_leave_apply_function(req, res) {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
await LeavesModel.findByIdAndDelete(leaveRequest._id)
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
} }

View File

@ -26,128 +26,136 @@ function formatHoursToReadable(hours) {
} }
} }
// Helper function to check daily hours and create logout log if needed // // Helper function to check daily hours and create logout log if needed
async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) { // async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
try { // try {
const today = new Date(); // const today = new Date();
const { start, end } = getDayRange(today); // const { start, end } = getDayRange(today);
// Get today's work entries for this VA and project // // Get today's work entries for this VA and project
// Exclude the current session if it's already in the database // // Exclude the current session if it's already in the database
const query = { // const query = {
vaId, // vaId,
projectId, // projectId,
from: { $gte: start, $lte: end } // from: { $gte: start, $lte: end }
}; // };
if (currentSessionId) { // if (currentSessionId) {
query._id = { $ne: currentSessionId }; // query._id = { $ne: currentSessionId };
} // }
const todayWorkEntries = await WorkTrackModel.find(query); // const todayWorkEntries = await WorkTrackModel.find(query);
// Calculate total hours worked today // // Calculate total hours worked today
let totalTodayMs = todayWorkEntries.reduce((total, entry) => { // let totalTodayMs = todayWorkEntries.reduce((total, entry) => {
return total + (entry.totalTime || 0); // return total + (entry.totalTime || 0);
}, 0); // }, 0);
// Add the additional time from current session if provided // // Add the additional time from current session if provided
totalTodayMs += additionalTimeMs; // totalTodayMs += additionalTimeMs;
const totalTodayHours = totalTodayMs / (1000 * 60 * 60); // const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
// Check if daily dedicated hours are completed // // Check if daily dedicated hours are completed
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) { // if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
// Create automatic logout log // // Create automatic logout log
const logoutLog = await LoggedModel.create({ // const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT", // loggedType: "LOGOUT",
vaId, // vaId,
vaEmail, // vaEmail,
projectId // projectId
}); // });
try { // try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = { // const sfDataSync = {
"_id": logoutLog._id, // "_id": logoutLog._id,
"loggedType": "LOGOUT", // "loggedType": "LOGOUT",
"vaId": vaId, // "vaId": vaId,
"vaEmail": vaEmail, // "vaEmail": vaEmail,
"projectId": projectId, // "projectId": projectId,
"time": logoutLog.time, // "time": logoutLog.time,
} // }
const response = await fetch(salesforceUrl, { // const response = await fetch(salesforceUrl, {
method: 'POST', // method: 'POST',
headers: { // headers: {
'Content-Type': 'application/json', // 'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication // // Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN // // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
}, // },
body: JSON.stringify(sfDataSync) // body: JSON.stringify(sfDataSync)
}); // });
if (!response.ok) { // if (!response.ok) {
const errorText = await response.text(); // const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
} // }
const result = await response.json(); // const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // // console.log("✅ Successfully synced VA data to Salesforce:", result);
return { // return {
shouldLogout: true, // shouldLogout: true,
totalTodayHours: totalTodayHours, // totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours, // dedicatedHours: project.dedicatedHours,
logoutLogId: logoutLog._id, // logoutLogId: logoutLog._id,
SyncSalesforce: result // SyncSalesforce: result
}; // };
} catch (error) { // } catch (error) {
console.error("Failed to sync with Salesforce:", error.message); // console.error("Failed to sync with Salesforce:", error.message);
console.log(error) // console.log(error)
// Create a custom error to distinguish Salesforce errors // // Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true; // salesforceError.isSalesforceError = true;
throw salesforceError; // throw salesforceError;
} // }
} // }
return { // return {
shouldLogout: false, // shouldLogout: false,
totalTodayHours: totalTodayHours, // totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours // dedicatedHours: project.dedicatedHours
}; // };
} catch (error) { // } catch (error) {
console.error("Error in checkDailyHoursAndLogout:", error); // console.error("Error in checkDailyHoursAndLogout:", error);
return { shouldLogout: false, error: error.message }; // return { shouldLogout: false, error: error.message };
} // }
} // }
// Getting logged hours in a specific project of VA // Getting logged hours in a specific project of VA
async function va_log_track_work_callback(req, res) { async function va_log_track_work_callback(req, res) {
try { try {
const accessToken = req.headers.token; // const accessToken = req.headers.token;
if (!accessToken) { // if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" }); // return res.status(401).json({ success: false, message: "Missing token" });
} // }
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); // const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) { // if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" }); // return res.status(401).json({ success: false, message: "Invalid token" });
} // }
const vaEmail = decoded.email; // const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); // const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) { // if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" }); // return res.status(404).json({ success: false, message: "VA not found" });
} // }
const vaId = vaUser._id; // const vaId = vaUser._id;
const payload = req.body || {}; const payload = req.body || {};
const { projectId, logId, title, appName, url = null } = payload;
let { vaId, projectId, logId, logs } = payload;
if (!projectId || !logId || !vaId || !Array.isArray(logs)) {
return res.status(400).json({ success: false, message: "Missing Props" });
}
const vaUser = await VAModel.findById(vaId);
if (!vaUser) return res.status(400).json({ success: false, message: "VA not found" });
const project = await ProjectModel.findById(projectId); const project = await ProjectModel.findById(projectId);
if (!project) return res.status(400).json({ success: false, message: "Project not found" }); if (!project) return res.status(400).json({ success: false, message: "Project not found" });
@ -155,117 +163,137 @@ async function va_log_track_work_callback(req, res) {
const logEntry = await LoggedModel.findById(logId); const logEntry = await LoggedModel.findById(logId);
if (!logEntry) return res.status(400).json({ success: false, message: "Log entry not found" }); if (!logEntry) return res.status(400).json({ success: false, message: "Log entry not found" });
// Check last log entry for this VA and project if (logs.length === 0) {
const lastLog = await LoggedModel.findOne({ vaId, projectId }) return res.status(400).json({ success: false, message: "No logs to track" });
.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) { logs[0].from = logEntry.time; // Ensure the first log's 'from' matches the main log time
return res.status(400).json({ success: false, message: "Missing Props" });
} const firstFromLogTime = new Date(logs[0].from);
const firstToLogTime = new Date(logs[0].to);
const firstTotalLogTime = Math.abs(firstToLogTime - firstFromLogTime);
logs[0].totalTime = firstTotalLogTime;
// // Check last log entry for this VA and project
// const lastLog = await LoggedModel.findOne({ vaId, projectId })
// .sort({ time: -1 }) // get the most recent log
// .lean();
// // If the last log is a LOGOUT, prevent new work tracking
// if (lastLog.loggedType === "LOGOUT") {
// return res.status(400).json({
// success: false,
// message: "User has already logged out from this project",
// });
// }
// Handle POST → create entry // Handle POST → create entry
if (req.method === "POST") { // if (req.method === "POST") {
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project); // const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
// const createdTracks = await Promise.all(
// logs.map(async (log) => {
// // 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,
// });
if (dailyCheck.shouldLogout) { // return createdTrack;
return res.status(400).json({ // })
success: false, // );
message: `Cannot start work - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`,
autoLogout: true const fromLogTime = new Date(logs[0].from);
}); const toLogTime = new Date(logs[logs.length -1].to);
} const totalLogTime = Math.abs(toLogTime - fromLogTime);
const createdTrack = await WorkTrackModel.create({ const createdTrack = await WorkTrackModel.create({
vaId, vaId,
projectId, projectId,
logId, logId,
title, from: logs[0].from,
appName, to: logs[logs.length -1].to,
url, totalTime: totalLogTime,
from: Date.now(), logs: logs
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: "Work tracking started", message: "Work tracking created",
data: createdTrack, data: createdTrack
dailyHours: {
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
remaining: formatHoursToReadable(dailyCheck.dedicatedHours - dailyCheck.totalTodayHours)
}
}); });
} // }
// Handle PATCH → update entry (add 'to' and calculate total time) // // Handle PATCH → update entry (add 'to' and calculate total time)
if (req.method === "PATCH") { // if (req.method === "PATCH") {
const { id } = payload; // expecting the workTrack record ID to update // const { id } = payload; // expecting the workTrack record ID to update
if (!id) { // if (!id) {
return res.status(400).json({ success: false, message: "Missing workTrack ID" }); // return res.status(400).json({ success: false, message: "Missing workTrack ID" });
} // }
const workEntry = await WorkTrackModel.findById(id); // const workEntry = await WorkTrackModel.findById(id);
if (!workEntry) { // if (!workEntry) {
return res.status(404).json({ success: false, message: "Work entry not found" }); // return res.status(404).json({ success: false, message: "Work entry not found" });
} // }
const toTime = new Date(); // const toTime = new Date();
const fromTime = new Date(workEntry.from); // const fromTime = new Date(workEntry.from);
const totalTimeMs = toTime - fromTime; // const totalTimeMs = toTime - fromTime;
// Optional: store totalTime in minutes or hours instead of raw ms // // Optional: store totalTime in minutes or hours instead of raw ms
const hours = Math.floor(totalTimeMs / (1000 * 60 * 60)); // const hours = Math.floor(totalTimeMs / (1000 * 60 * 60));
const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60)); // const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000); // const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000);
const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`; // const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`;
// Check if daily dedicated hours are completed after this work session // // Check if daily dedicated hours are completed after this work session
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs); // const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
workEntry.to = toTime; // workEntry.to = toTime;
workEntry.totalTime = totalTimeMs; // store as number of minutes // workEntry.totalTime = totalTimeMs; // store as number of minutes
await workEntry.save(); // await workEntry.save();
const response = { // const response = {
success: true, // success: true,
message: "Work tracking stopped", // message: "Work tracking stopped",
data: { // data: {
id: workEntry._id, // id: workEntry._id,
from: workEntry.from, // from: workEntry.from,
to: workEntry.to, // to: workEntry.to,
formattedTotalTime, // formattedTotalTime,
totalTimeMs, // totalTimeMs,
sessionHours: totalTimeMs / (1000 * 60 * 60) // sessionHours: totalTimeMs / (1000 * 60 * 60)
}, // },
dailyHours: { // dailyHours: {
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours), // todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours), // dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours)) // remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
} // }
}; // };
// If auto-logout was triggered, add that info to response // // If auto-logout was triggered, add that info to response
if (dailyCheck.shouldLogout) { // if (dailyCheck.shouldLogout) {
response.autoLogout = true; // response.autoLogout = true;
response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`; // response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
response.logoutLogId = dailyCheck.logoutLogId; // response.logoutLogId = dailyCheck.logoutLogId;
} // }
return res.status(200).json(response); // return res.status(200).json(response);
} // }
// Fallback for unsupported methods // Fallback for unsupported methods
return res.status(405).json({ success: false, message: "Method not allowed" }); // return res.status(405).json({ success: false, message: "Method not allowed" });
} catch (error) { } catch (error) {
return res.status(500).send("Internal Server Error"); return res.status(500).send("Internal Server Error");
} }

View File

@ -1,4 +1,5 @@
import { VAModel } from "../../../models/vaModel.js"; import { VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
async function va_payslip_data_function(req, res) { async function va_payslip_data_function(req, res) {
try { try {
@ -9,9 +10,9 @@ async function va_payslip_data_function(req, res) {
return res.status(404).json({ success: false, message: "VA not found" }); 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=${vaId}`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip/${vaId}`;
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,8 +1,10 @@
import authFetch from "../../../utils/authFetch.js";
async function va_payroll_schedule_function(req, res) { async function va_payroll_schedule_function(req, res) {
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/payroll-schedule`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/payroll-schedule`;
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -44,7 +44,7 @@ async function va_remove_function(req, res) {
if (projectIds.length > 0) { if (projectIds.length > 0) {
const projectResult = await ProjectModel.updateMany( const projectResult = await ProjectModel.updateMany(
{ _id: { $in: projectIds } }, { _id: { $in: projectIds } },
{ $pull: { vaAssigned: vaId } } { $pull: { "vaAssigned.vaId": vaId } }
); );
projectsUpdated = projectResult.modifiedCount; projectsUpdated = projectResult.modifiedCount;
console.log(`Removed VA from ${projectsUpdated} projects`); console.log(`Removed VA from ${projectsUpdated} projects`);

View File

@ -21,16 +21,12 @@ async function va_profile_callback(req, res) {
return res.status(400).json({ success: false, message: "Invalid token payload" }); return res.status(400).json({ success: false, message: "Invalid token payload" });
} }
console.log(decoded.email , "Line 24")
const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }) const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email })
.select("-userDetails.password") // hide password .select("-userDetails.password") // hide password
.populate("clientIds", "userDetails.email userDetails.name") // optional populate .populate("clientIds", "userDetails.email userDetails.name") // optional populate
.populate("projectIds", "name totalLoggedHours") // optional populate .populate("projectIds", "name totalLoggedHours") // optional populate
.lean(); .lean();
console.log("Line 33" , vaUser)
if (!vaUser) { if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" }); return res.status(404).json({ success: false, message: "VA not found" });
} }

View File

@ -5,7 +5,7 @@ import { VAModel } from "../../../models/vaModel.js";
// Register a new VA // Register a new VA
async function va_profile_update_callback(req, res) { async function va_profile_update_callback(req, res) {
try { try {
const sf_token = req.headers.sf_token; const sf_token = req.headers["x-auth-token"];
const accessToken = req.headers.token; const accessToken = req.headers.token;
let vaUser; let vaUser;
@ -68,7 +68,7 @@ async function va_profile_update_callback(req, res) {
const allowedFields = [ const allowedFields = [
"firstName", "lastName", "dob", "age", "address", "firstName", "lastName", "dob", "age", "address",
"city", "state", "country", "phone", "skype", "city", "state", "country", "phone", "skype",
"paymentMethod", "payoneerAccount", "veemAccount" "paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail"
]; ];
let hasUpdates = false; let hasUpdates = false;

View File

@ -1,5 +1,6 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { VAModel } from "../../../models/vaModel.js"; import { VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Register a new VA // Register a new VA
async function va_register(req, res) { async function va_register(req, res) {
@ -53,7 +54,7 @@ async function va_register(req, res) {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
delete created.userDetails._id; delete created.userDetails._id;
created.userDetails._id = created._id; // Include VA's MongoDB ID created.userDetails._id = created._id; // Include VA's MongoDB ID
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -74,7 +75,7 @@ async function va_register(req, res) {
res.status(201).json({ success: true, data: created, SyncSalesforce: result }); res.status(201).json({ success: true, data: created, SyncSalesforce: result });
} catch (error) { } catch (error) {
console.error("❌ Failed to sync with Salesforce:", error.message); console.error("❌ Failed to sync with Salesforce:", error.message);
await VAModel.findOneAndDelete({_id: created.userDetails._id});
// Create a custom error to distinguish Salesforce errors // Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true; salesforceError.isSalesforceError = true;

View File

@ -1,4 +1,7 @@
import mongoose from "mongoose";
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js"; import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
import getDayNumber from "../../../utils/getDayNumber.js";
// Helper function to get start and end of day in UTC // Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) { function getDayRange(date = new Date()) {
@ -105,7 +108,7 @@ function getCurrentUTCTime() {
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`); // console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
// // Check for PARTTIME with flexible schedule // // Check for PARTTIME with flexible schedule
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) { // if (vaStatus === "PARTTIME" && project.isFlexibleAllowed) {
// return { isCompliant: true, warnings }; // return { isCompliant: true, warnings };
// } // }
@ -189,15 +192,15 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = { const sfDataSync = {
"_id": logoutLog._id, "_id": logoutLog._id,
"loggedType": "LOGOUT", "loggedType": "LOGOUT",
"vaId": vaId, "vaId": vaId,
"vaEmail": lastLog.vaEmail, "vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId, "projectId": lastLog.projectId,
"time": logoutLog.time, "time": logoutLog.time,
} }
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -289,6 +292,129 @@ async function va_project_log_callback(req, res) {
// }); // });
// } // }
// If Logout then Update work track entries
if (type === "LOGOUT") {
const { logId } = payload;
const workTrackData = await WorkTrackModel.find({
logId: new mongoose.Types.ObjectId(logId)
});
// console.log(`Found work track data for logout:`, workTrackData);
if (workTrackData[0]) {
const fromLogTime = new Date(workTrackData[0].from);
const toLogTime = new Date(logTime);
const totalTimeMs = toLogTime - fromLogTime;
const updatedLogs = workTrackData[0].logs ? [...workTrackData[0].logs] : [];
if (updatedLogs.length > 0) {
const lastLogIndex = updatedLogs.length - 1;
const lastLogEntry = updatedLogs[lastLogIndex];
// Update the last log's 'to' time and recalculate its totalTime
const lastLogFromTime = new Date(lastLogEntry.from);
const lastLogTotalTime = Math.abs(toLogTime - lastLogFromTime);
updatedLogs[lastLogIndex] = {
...lastLogEntry,
to: toLogTime,
totalTime: lastLogTotalTime
};
// console.log(`Updated last log in array - from: ${lastLogFromTime.toISOString()}, to: ${toLogTime.toISOString()}, totalTime: ${lastLogTotalTime}ms`);
}
const updateResult = await WorkTrackModel.updateOne(
{ _id: workTrackData[0]._id },
{
$set: {
to: toLogTime,
totalTime: totalTimeMs,
logs: updatedLogs
}
}
);
console.log(`Updated work track ${workTrackData[0]._id}:`, {
from: fromLogTime.toISOString(),
to: toLogTime.toISOString(),
totalTime: totalTimeMs,
logsUpdated: updatedLogs.length,
mongodbUpdateResult: updateResult
});
// const updatedWorkTrack = await WorkTrackModel.findById(workTrackData[0]._id);
// console.log("Verified updated work track:", {
// to: updatedWorkTrack?.to,
// totalTime: updatedWorkTrack?.totalTime,
// logsCount: updatedWorkTrack?.logs?.length,
// lastLog: updatedWorkTrack?.logs?.[updatedWorkTrack.logs.length - 1]
// });
} else {
console.log(`No work track found for vaId: ${vaId}, projectId: ${projectId}, logId: ${logId}`);
}
// Create the logout log entry
const createdLog = await LoggedModel.create({
loggedType: type,
vaId,
vaEmail,
projectId,
time: logTime
});
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
"_id": createdLog._id,
"loggedType": type,
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": createdLog.time,
}
const response = await authFetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(sfData)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
return res.status(201).json({
success: true,
message: `${type} Successfully`,
logId: createdLog._id,
SyncSalesforce: result,
timeUsed: receivedTimeString ? 'received' : 'current',
actualTime: createdLog.time,
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Still return success since the logout was recorded locally
return res.status(201).json({
success: true,
message: `${type} Successfully (Salesforce sync failed)`,
logId: createdLog._id,
timeUsed: receivedTimeString ? 'received' : 'current',
actualTime: createdLog.time,
warning: "Salesforce sync failed"
});
}
}
// For LOGIN requests, check daily hours (using UTC day range) // For LOGIN requests, check daily hours (using UTC day range)
if (type === "LOGIN") { if (type === "LOGIN") {
const { start, end } = getDayRange(); // UTC day range const { start, end } = getDayRange(); // UTC day range
@ -305,11 +431,13 @@ async function va_project_log_callback(req, res) {
const totalTodayHours = totalTodayMs / (1000 * 60 * 60); const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
const dayNumber = getDayNumber(logTime);
// Check if daily hours are already completed // Check if daily hours are already completed
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) { if (project.dedicatedHours[dayNumber] > 0 && totalTodayHours >= project.dedicatedHours[dayNumber]) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`, message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours[dayNumber])})`,
// warnings: scheduleCheck.warnings // warnings: scheduleCheck.warnings
}); });
} }
@ -340,15 +468,15 @@ async function va_project_log_callback(req, res) {
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = { const sfData = {
"_id": createdLog._id, "_id": createdLog._id,
"loggedType": "LOGIN", "loggedType": "LOGIN",
"vaId": vaId, "vaId": vaId,
"vaEmail": vaEmail, "vaEmail": vaEmail,
"projectId": projectId, "projectId": projectId,
"time": createdLog.time, "time": createdLog.time,
} }
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -366,7 +494,7 @@ async function va_project_log_callback(req, res) {
const result = await response.json(); const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: `${type} Successfully`, message: `${type} Successfully`,
logId: createdLog._id, logId: createdLog._id,
@ -402,15 +530,15 @@ async function va_project_log_callback(req, res) {
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = { const sfData = {
"_id": createdLog._id, "_id": createdLog._id,
"loggedType": type, "loggedType": type,
"vaId": vaId, "vaId": vaId,
"vaEmail": vaEmail, "vaEmail": vaEmail,
"projectId": projectId, "projectId": projectId,
"time": createdLog.time, "time": createdLog.time,
} }
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

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

View File

@ -11,7 +11,7 @@ function formatHMS(ms) {
async function va_projects_report_function(req, res) { async function va_projects_report_function(req, res) {
try { try {
const sf_token = req.headers.sf_token; const sf_token = req.headers["x-auth-token"];
let vaId; let vaId;
let vaUser; let vaUser;
let vaEmail; let vaEmail;
@ -49,9 +49,38 @@ async function va_projects_report_function(req, res) {
// Default: current day range // Default: current day range
const now = new Date(); const now = new Date();
const fromDate = from && from !== "undefined" ? new Date(from) : new Date(now.setHours(0, 0, 0, 0)); let fromDate, toDate;
const toDate = to && to !== "undefined" ? new Date(to) : new Date();
toDate.setHours(23, 59, 59, 999); 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"
});
}
// Get all VA login/logout logs for this project in range // Get all VA login/logout logs for this project in range
const logs = await LoggedModel.find({ const logs = await LoggedModel.find({
@ -61,9 +90,36 @@ async function va_projects_report_function(req, res) {
}).sort({ time: 1 }).lean(); }).sort({ time: 1 }).lean();
if (!logs.length) { if (!logs.length) {
return res.json({ success: true, message: "No logs found for this project", data: [] }); 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))
}
});
} }
// Get all work tracks for this VA and project in the date range
const workTracks = await WorkTrackModel.find({
projectId,
vaId,
$or: [
{ from: { $gte: fromDate, $lte: toDate } },
{ to: { $gte: fromDate, $lte: toDate } },
{
$and: [
{ from: { $lte: fromDate } },
{ to: { $gte: fromDate } }
]
}
]
})
.sort({ from: 1 })
.lean();
// Unique VA IDs // Unique VA IDs
const vaIds = [...new Set(logs.map(log => log.vaId?.toString()))].filter(Boolean); const vaIds = [...new Set(logs.map(log => log.vaId?.toString()))].filter(Boolean);
const vaUsers = await VAModel.find( const vaUsers = await VAModel.find(
@ -73,55 +129,120 @@ async function va_projects_report_function(req, res) {
const projectReport = []; const projectReport = [];
for (const vaId of vaIds) { for (const currentVaId of vaIds) {
const vaLogList = logs.filter(l => l.vaId.toString() === vaId); const vaLogList = logs.filter(l => l.vaId.toString() === currentVaId);
let sessions = []; let sessions = [];
let currentLogin = null; let currentLogin = null;
const vaWorkTracks = workTracks.filter(w => w.vaId.toString() === currentVaId);
// Pair login → logout // Pair login → logout
for (const entry of vaLogList) { for (const entry of vaLogList) {
if (entry.loggedType === "LOGIN") { if (entry.loggedType === "LOGIN") {
currentLogin = entry; currentLogin = entry;
} else if (entry.loggedType === "LOGOUT" && currentLogin) { } else if (entry.loggedType === "LOGOUT" && currentLogin) {
sessions.push({ from: currentLogin.time, to: entry.time }); sessions.push({ from: currentLogin.time, to: entry.time, logId: currentLogin._id });
currentLogin = null; currentLogin = null;
} }
} }
// If still logged in, close at end of day // If still logged in, close at end of day
if (currentLogin) { if (currentLogin) {
sessions.push({ from: currentLogin.time, to: toDate }); sessions.push({ from: currentLogin.time, to: toDate, logId: currentLogin._id });
} }
const vaDetails = vaUsers.find(v => v._id.toString() === vaId); const vaDetails = vaUsers.find(v => v._id.toString() === currentVaId);
let totalWorkMs = 0; let totalWorkMs = 0;
const sessionDetails = []; const sessionDetails = [];
for (const s of sessions) { // for (const s of sessions) {
const workData = await WorkTrackModel.find({ // const workData = await WorkTrackModel.find({
vaId, // vaId,
projectId, // projectId,
from: { $gte: s.from }, // from: { $gte: s.from },
to: { $lte: s.to } // to: { $lte: s.to }
}).sort({ from: 1 }).lean(); // }).sort({ from: 1 }).lean();
const tabs = workData.map(t => ({ // const tabs = workData.map(t => ({
title: t.title, // title: t.title,
appName: t.appName, // appName: t.appName,
from: t.from, // from: t.from,
to: t.to, // to: t.to,
durationMs: t.totalTime, // durationMs: t.totalTime,
durationFormatted: formatHMS(t.totalTime), // durationFormatted: formatHMS(t.totalTime),
})); // }));
const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0); // const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0);
totalWorkMs += sessionWorkMs; // totalWorkMs += sessionWorkMs;
// sessionDetails.push({
// from: s.from,
// to: s.to,
// totalSessionTime: formatHMS(sessionWorkMs),
// tabs,
// });
// }
for (const session of sessions) {
// Find work tracks for this specific session (matching logId)
const sessionWorkTracks = vaWorkTracks.filter(
track => track.logId && track.logId.toString() === session.logId.toString()
);
let sessionTabs = [];
let sessionTotalMs = 0;
// Extract all logs from work tracks for this session
for (const workTrack of sessionWorkTracks) {
if (workTrack.logs && Array.isArray(workTrack.logs)) {
// Process each log entry in the logs array
for (const logEntry of workTrack.logs) {
// Calculate duration for this log entry
const logFrom = new Date(logEntry.from);
const logTo = new Date(logEntry.to || workTrack.to || session.to);
const logDuration = logEntry.totalTime || (logTo - logFrom);
sessionTotalMs += logDuration;
sessionTabs.push({
title: logEntry.title || "",
appName: logEntry.appName || "",
url: logEntry.url || null,
from: logFrom,
to: logTo,
durationMs: logDuration,
durationFormatted: formatHMS(logDuration),
});
}
}
// Also add the main work track if it has separate data
if (workTrack.totalTime && workTrack.totalTime > 0) {
sessionTotalMs += workTrack.totalTime;
// Only add if we don't have logs array data
if (!workTrack.logs || workTrack.logs.length === 0) {
sessionTabs.push({
title: workTrack.title || "",
appName: workTrack.appName || "",
url: workTrack.url || null,
from: workTrack.from,
to: workTrack.to,
durationMs: workTrack.totalTime,
durationFormatted: formatHMS(workTrack.totalTime),
});
}
}
}
totalWorkMs += sessionTotalMs;
sessionDetails.push({ sessionDetails.push({
from: s.from, from: session.from,
to: s.to, to: session.to,
totalSessionTime: formatHMS(sessionWorkMs), totalSessionTime: formatHMS(sessionTotalMs),
tabs, totalSessionMs: sessionTotalMs,
workTracksCount: sessionWorkTracks.length,
tabs: sessionTabs.sort((a, b) => a.from - b.from), // Sort tabs by time
}); });
} }

View File

@ -1,15 +1,24 @@
import mongoose from "mongoose";
import { AdjustedHoursModel } from "../../../models/vaModel.js"; import { AdjustedHoursModel } from "../../../models/vaModel.js";
// Get all adjustment requests (with filtering options) // Get all adjustment requests (with filtering options)
async function get_adjustment_requests_list(req, res) { async function get_adjustment_requests_list(req, res) {
try { try {
const { status, projectId, vaId } = req.params; const { status, projectId, vaId } = req.query;
// Build filter object // Build filter object
const filter = {}; const filter = {};
if (status) filter.status = status; // if (status) filter.status = status;
if (projectId) filter.projectId = projectId; // if (projectId) filter.projectId = projectId;
if (vaId) filter.vaId = vaId; // if (vaId) filter.vaId = vaId;
if (status && status !== "undefined") filter.status = status;
if (projectId && mongoose.Types.ObjectId.isValid(projectId)) {
filter.projectId = new mongoose.Types.ObjectId(projectId);
}
if (vaId && mongoose.Types.ObjectId.isValid(vaId)) {
filter.vaId = new mongoose.Types.ObjectId(vaId);
}
const adjustments = await AdjustedHoursModel.find(filter) const adjustments = await AdjustedHoursModel.find(filter)
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })

View File

@ -1,5 +1,6 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js"; import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Adjust missing hours by creating new records // Adjust missing hours by creating new records
async function va_timesheet_adjustment_function(req, res) { async function va_timesheet_adjustment_function(req, res) {
@ -34,7 +35,7 @@ async function va_timesheet_adjustment_function(req, res) {
// Validate project exists and VA is assigned to it // Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({ const project = await ProjectModel.findOne({
_id: projectId, _id: projectId,
vaAssigned: vaId "vaAssigned.vaId": vaId
}); });
if (!project) { if (!project) {
@ -148,14 +149,14 @@ async function va_timesheet_adjustment_function(req, res) {
"date": adjustmentDate.toISOString().split('T')[0], "date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(), "loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(), "logoutTime": logoutDateTime.toISOString(),
"comment": processedComment.plainText, // Send plain text to Salesforce "comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
"attachments": processedComment.extractedImages, // Send extracted images info "attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING", "status": "PENDING",
"rejectionReason": "", "rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds) "adjustedHours": formatTime(adjustedSeconds)
} }
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -167,6 +168,7 @@ async function va_timesheet_adjustment_function(req, res) {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
await AdjustedHoursModel.findByIdAndDelete(adjustmentRequest._id)
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
} }

View File

@ -1,9 +1,10 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
import getDayNumber from "../../../utils/getDayNumber.js";
async function va_timesheet_summary_function(req, res) { async function va_timesheet_summary_function(req, res) {
try { try {
const sf_token = req.headers.sf_token; const sf_token = req.headers["x-auth-token"];
let vaId; let vaId;
let vaUser; let vaUser;
let vaEmail; let vaEmail;
@ -43,7 +44,7 @@ async function va_timesheet_summary_function(req, res) {
// Validate project exists and VA is assigned to it // Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({ const project = await ProjectModel.findOne({
_id: projectId, _id: projectId,
vaAssigned: vaId "vaAssigned.vaId": vaId
}); });
if (!project) { if (!project) {
@ -162,8 +163,10 @@ async function va_timesheet_summary_function(req, res) {
} }
}; };
const dayNumber = getDayNumber();
// Calculate dedicated hours and pending hours // Calculate dedicated hours and pending hours
const dedicatedHours = project.dedicatedHours || 0; const dedicatedHours = project.dedicatedHours[dayNumber] || 0;
const totalWorkedHours = totalWorkSeconds / 3600; // Convert to hours const totalWorkedHours = totalWorkSeconds / 3600; // Convert to hours
const pendingHours = Math.max(0, dedicatedHours - totalWorkedHours); const pendingHours = Math.max(0, dedicatedHours - totalWorkedHours);

View File

@ -1,4 +1,5 @@
import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js"; import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Approve or reject leave // Approve or reject leave
async function va_tips_manage_function(req, res) { async function va_tips_manage_function(req, res) {
@ -38,7 +39,7 @@ async function va_tips_manage_function(req, res) {
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
} }
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -116,7 +117,7 @@ async function va_tips_delete(req, res) {
// "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents // "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
// } // }
const response = await fetch(salesforceUrl, { const response = await authFetch(salesforceUrl, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

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

View File

@ -101,8 +101,8 @@ const projectSchema = new Schema({
} }
], ],
dedicatedHours: { dedicatedHours: {
type: String, type: Array,
default: "0" default: ["0", "0", "0", "0", "0", "0", "0"]
}, },
status: { status: {
type: String, type: String,
@ -136,6 +136,12 @@ const clientDetailsSchema = new Schema({
name: { name: {
type: String, type: String,
}, },
firstName: {
type: String,
},
lastName: {
type: String,
},
email: { email: {
type: String, type: String,
required: true, required: true,
@ -344,15 +350,6 @@ const vaWorkTrackSchema = new Schema({
type: Types.ObjectId, type: Types.ObjectId,
ref: "TrackLog", ref: "TrackLog",
}, },
title: {
type: String,
},
appName: {
type: String,
},
url: {
type: String,
},
from: { from: {
type: Date, type: Date,
default: Date.now, default: Date.now,
@ -365,6 +362,10 @@ const vaWorkTrackSchema = new Schema({
type: Number, type: Number,
default: 0, default: 0,
}, },
logs: {
type: Array,
default: [],
}
}, },
{ timestamps: true } { timestamps: true }
) )

View File

@ -14,6 +14,8 @@ import { client_verify_email } from "../../controllers/client/vaClientEmailVerif
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js"; import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js"; import { 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 { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js";
import { clientDashboardGetVaLogsController } from "../../controllers/client/dashboard/clientDashboardGetVaLogs.controller.js";
import { va_credits_get } from "../../controllers/client/dashboard/clientDashboardCredits.controller.js";
// clients routes // clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/register", va_register_client); // Register a new VA
@ -28,6 +30,10 @@ clientRoutes.get("/tips", clientAuthValidator, va_tips_get);
clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update); clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update);
clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete); clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete);
clientRoutes.get("/credits", clientAuthValidator, va_credits_get);
clientRoutes.get("/vaLogsData/:vaId", clientAuthValidator, clientDashboardGetVaLogsController);
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) // clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)

View File

@ -13,13 +13,14 @@ import { project_add_sf } from "../../controllers/project/projectAddSf.controlle
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js" import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
import { authValidator } from "../../middlewares/auth.middleware.js"; import { authValidator } from "../../middlewares/auth.middleware.js";
import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js" import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js"
import { createWireGuardConnection, disconnectWireGuardConnection } from '../../vpnHandler/vpn.js';
const v1Routes = express.Router() const v1Routes = express.Router()
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
v1Routes.get("/auth", function(req, res) { v1Routes.get("/auth", function (req, res) {
res.sendFile(path.join(__dirname, '/auth.html')); res.sendFile(path.join(__dirname, '/auth.html'));
}); });
@ -34,9 +35,50 @@ v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
v1Routes.post("/password/reset", process_reset); v1Routes.post("/password/reset", process_reset);
v1Routes.get("/", function(req, res) { v1Routes.get("/", function (req, res) {
res.json({"connected": true}) res.json({ "connected": true })
}) })
v1Routes.post("/vpn/connect", (req, res) => {
try {
const data = createWireGuardConnection();
res.json({
success: true,
message: "VPN connection created",
...data
});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
});
v1Routes.post("/vpn/disconnect", (req, res) => {
try {
const { ip } = req.body;
if (!ip) {
return res.status(400).json({
success: false,
error: "IP address is required"
});
}
const result = disconnectWireGuardConnection(ip);
if (result && result.success) {
return res.status(200).json(result);
}
return res.status(400).json({success: false, error: result.message});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
});
// v1Routes.use("/form_submit", authValidator, formRoutes); // v1Routes.use("/form_submit", authValidator, formRoutes);
export default v1Routes; export default v1Routes;

View File

@ -30,7 +30,7 @@ import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.cont
import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.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_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA vaRoutes.get("/", va_callback); // VA Callback for getting active VA list
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
@ -47,7 +47,9 @@ vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
// timesheet adjustment routes // timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date vaRoutes.get("/timesheet/summary/:projectId/:date/: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.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
// vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject 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.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
// Leaves routes // Leaves routes
@ -56,7 +58,7 @@ vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); /
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function); vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function);
// timesheet adjustment routes // timesheet adjustment routes
@ -74,7 +76,7 @@ vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_project
vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project
vaRoutes.post("/log/project/track", authValidator, va_log_track_work_callback); // Track Window Tabs Logs Data vaRoutes.post("/log/project/track", authValidator, va_log_track_work_callback); // Track Window Tabs Logs Data
vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time // vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time
vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); // VA Audit Report // All Projects vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); // VA Audit Report // All Projects
vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report
// VA filter search: supports query params `name`, `email`, `from`, `to` // VA filter search: supports query params `name`, `email`, `from`, `to`
@ -84,9 +86,6 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba
vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule
vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback vaRoutes.get("/:userName", va_username_callback); // VA Username Callback

View File

@ -144,19 +144,13 @@ app.get('/clientregister/:sf_token?', (req, res) => {
app.get('/auth/password/reset-form', render_reset_form); 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) => { app.get("/client/login", (req, res) => {
// res.render("client_login")
res.render("client/client_login", { res.render("client/client_login", {
layout: "layouts/auth", // ⬅️ override layout layout: "layouts/auth", // ⬅️ override layout
title: "DocVA | Login" title: "DocVA | Login"
}); });
}); })
app.get("/client/forgot-password", (req, res) => { app.get("/client/forgot-password", (req, res) => {
@ -170,11 +164,12 @@ app.get("/client/forgot-password", (req, res) => {
app.get('/client/dashboard', async (req, res) => { app.get('/client/dashboard', async (req, res) => {
try { try {
// const dashboardData = await dashboardService.getClientDashboard(req.user.id); // const dashboardData = await dashboardService.getClientDashboard(req.user.id);
// res.render('client/dashboard-dashboard', dashboardData);
// res.render('client/dashboard-dashboard');
res.render('client/dashboard-dashboard', { res.render('client/dashboard-dashboard', {
title: "Client Dashboard", title: "Client Dashboard",
pageScript: "dashboard.js" pageScript: "dashboard.js"
}); });
// res.render('layouts/dashboard');
} catch (error) { } catch (error) {
console.error('Dashboard error:', error); console.error('Dashboard error:', error);
res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' });
@ -184,6 +179,7 @@ app.get('/client/dashboard', async (req, res) => {
app.get('/client/virtual-assistants', async (req, res) => { app.get('/client/virtual-assistants', async (req, res) => {
try { try {
// const vaData = await virtualAssistantService.getClientVAs(req.user.id); // const vaData = await virtualAssistantService.getClientVAs(req.user.id);
// res.render('client/dashboard-virtual-assistants');
res.render('client/dashboard-virtual-assistants', { res.render('client/dashboard-virtual-assistants', {
title: "Virtual Assistants", title: "Virtual Assistants",
pageScript:"dashboard-virtual-assistants.js" pageScript:"dashboard-virtual-assistants.js"
@ -199,6 +195,7 @@ app.get('/client/virtual-assistants', async (req, res) => {
app.get('/client/projects', async (req, res) => { app.get('/client/projects', async (req, res) => {
try { try {
// const vaData = await virtualAssistantService.getClientVAs(req.user.id); // const vaData = await virtualAssistantService.getClientVAs(req.user.id);
// res.render('client/dashboard-projects');
res.render('client/dashboard-projects', { res.render('client/dashboard-projects', {
title: "Projects", title: "Projects",
pageScript:"projects.js" pageScript:"projects.js"
@ -213,10 +210,10 @@ app.get('/client/projects', async (req, res) => {
app.get('/client/profile', async (req, res) => { app.get('/client/profile', async (req, res) => {
try { try {
// const vaData = await virtualAssistantService.getClientVAs(req.user.id); // const vaData = await virtualAssistantService.getClientVAs(req.user.id);
// res.render('client/dashboard-profile');
res.render('client/dashboard-profile',{ res.render('client/dashboard-profile',{
title: "Profile", title: "Profile",
pageScript:"profile.js" pageScript:"profile.js"
}); });
} catch (error) { } catch (error) {
console.error('Projects error:', error); console.error('Projects error:', error);
@ -224,11 +221,44 @@ app.get('/client/profile', async (req, res) => {
} }
}); });
app.get('/client/virtual-assistants/manage-tips', (req, res) => { app.get('/client/virtual-assistants/manage-tips', async (req, res) => {
res.render('client/dashboard-manage-va-tips', { try {
title: 'Manage VA Tips', // const vaData = await virtualAssistantService.getClientVAs(req.user.id);
pageScript: 'manage-tips.js' // 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' });
}
}); });
@ -267,20 +297,20 @@ app.get("/live/:projectId/:vaId", async (req, res) => {
// app.get("/live/:projectId/:vaId", async (req, res) => { // app.get("/live/:projectId/:vaId", async (req, res) => {
// const { projectId, vaId } = req.params; // const { projectId, vaId } = req.params;
// if (!projectId || !vaId) { // if (!projectId || !vaId) {
// return res.status(400).send({ "message": "Page not found" }); // return res.status(400).send({ "message": "Page not found" });
// } // }
// let clientId = null; // let clientId = null;
// try { // try {
// const clientRes = await ProjectModel.findById(projectId); // const clientRes = await ProjectModel.findById(projectId);
// if (clientRes) { // if (clientRes) {
// clientId = clientRes.clientId.toString(); // clientId = clientRes.clientId.toString();
// } // }
// } catch (err) { // } catch (err) {
// console.error("Error fetching client for project:", err.message); // console.error("Error fetching client for project:", err.message);
// return; // return;
// } // }
// if (!clientId) return res.status(400).send({ "message": "Page not found" }); // if (!clientId) return res.status(400).send({ "message": "Page not found" });
@ -362,6 +392,8 @@ io.use(async (socket, next) => {
if (clientUser) { if (clientUser) {
socket.data.role = "client"; socket.data.role = "client";
socket.data.clientId = clientUser._id.toString(); socket.data.clientId = clientUser._id.toString();
socket.data.vaId = vaId.toString();
socket.data.projectId = projectId;
// Get all VA IDs assigned to this client // Get all VA IDs assigned to this client
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString()); socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
@ -382,7 +414,7 @@ io.on("connection", (socket) => {
console.log("User connected:", socket.id); console.log("User connected:", socket.id);
// Register role and ID (client or VA) // Register role and ID (client or VA)
socket.on("register", async ({ role, id }) => { socket.on("register", async ({ role, id, projectId = null, clientId = null }) => {
if (role === "va") { if (role === "va") {
socket.data.role = "va"; socket.data.role = "va";
socket.data.vaId = id; socket.data.vaId = id;
@ -391,23 +423,35 @@ io.on("connection", (socket) => {
if (role === "client") { if (role === "client") {
socket.data.role = "client"; socket.data.role = "client";
socket.data.clientId = id; socket.data.clientId = clientId;
// 🧩 Get allowed VAs for this client // 🧩 Get allowed VAs for this client
const client = await ClientModel.findById(id) const client = await ClientModel.findById(clientId);
.populate("projectIds") // to access their projects
.populate("vaIds"); // optional if you track directly
// Collect all VA IDs from their projects const a = JSON.stringify(client);
const projectVAIds = new Set(); console.log(a, "client data for allowed VAs");
for (const project of client.projectIds) { if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
const populated = await ProjectModel.findById(project._id).populate("vaAssigned"); const projectVAIds = new Set();
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
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 });
} }
// Store allowed VA IDs in socket memory // Collect all VA IDs from their projects
socket.data.allowedVAs = Array.from(projectVAIds); // const projectVAIds = new Set();
console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs); // 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);
} }
}); });
@ -507,7 +551,6 @@ io.on("connection", (socket) => {
} }
const vaId = vaUser._id.toString(); const vaId = vaUser._id.toString();
console.log("🟢 Received Apps from VA:", vaId);
// Broadcast image only to allowed clients // Broadcast image only to allowed clients
for (let [_, clientSocket] of io.sockets.sockets) { for (let [_, clientSocket] of io.sockets.sockets) {
@ -538,15 +581,58 @@ io.on("connection", (socket) => {
); );
if (vaSocketId) { if (vaSocketId) {
io.to(vaSocketId).emit("stopSendAppsDetails"); // io.to(vaSocketId).emit("stopSendAppsDetails");
io.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("stopSendAppsDetails", { projId: projectId });
} }
} }
console.log("❌ Client disconnected");
}); });
}); });
app.get("/health", (req, res) => {
return res.status(200).json({ "success": true, "message": "Server is Healthy" })
})
app.post("/post-test", (req, res) => {
console.log("✅ POST request received at /post-test:", req.body);
return res.status(200).json({ "success": true, "message": "POST request received", "data": req.body })
});
app.get("/tnyk/:sf_token", (req, res) => {
const sf_token = req.params.sf_token;
console.log("Received token:", sf_token);
try {
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
if (decoded) {
res.render('client/dashboard-dashboard', { sf_token, title: "Client" });
} else {
res.render('not_found', { title: "Client" });
}
} catch (error) {
res.render('not_found', { title: "Client" });
}
});
app.get("/", (req, res) => {
// res.render("client_login")
res.render("client/client_login", {
layout: "layouts/auth", // ⬅️ override layout
title: "DocVA | Login"
});
})
app.get("/forgot-password", (req, res) => {
res.render("forgot_password", {
title: "Forgot Password",
layout: "layouts/forgotpasswordlayout"
});
})
app.use((req, res) => { app.use((req, res) => {
res.status(404).render("not_found", { res.status(404).render("not_found", {
layout: "layouts/error", layout: "layouts/error",
@ -554,7 +640,6 @@ app.use((req, res) => {
}); });
}); });
server.listen(PORT, () => { server.listen(PORT, () => {
mongoose mongoose
.connect( .connect(

View File

@ -3,6 +3,7 @@ import bcrypt from "bcrypt";
import { VAModel, ClientModel } from "../models/vaModel.js"; import { VAModel, ClientModel } from "../models/vaModel.js";
import logger from "../config/logger.js"; import logger from "../config/logger.js";
import axios from "axios"; import axios from "axios";
import authFetch from "../utils/authFetch.js";
class PasswordResetService { class PasswordResetService {
constructor(passwordResetRepository) { constructor(passwordResetRepository) {
@ -63,7 +64,7 @@ class PasswordResetService {
logger.info(`Password reset token created for ${userType}: ${email}`); logger.info(`Password reset token created for ${userType}: ${email}`);
return { return {
success: true, success: true,
message: "If the email exists, a reset link has been sent" message: "Email sent successfully. Please check your inbox."
}; };
} catch (error) { } catch (error) {
@ -169,14 +170,19 @@ class PasswordResetService {
try { try {
const userTypeText = userType === 'va' ? 'VA' : 'CLIENT'; const userTypeText = userType === 'va' ? 'VA' : 'CLIENT';
await axios.post(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, { const res = await authFetch(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, {
data: { method: 'POST',
email: email, headers: {
url: resetLink, 'Content-Type': 'application/json',
userType: userTypeText },
} body: JSON.stringify({
data: {
email: email,
url: resetLink,
userType: userTypeText
}
})
}); });
} catch (error) { } catch (error) {
logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`); logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`);
throw new Error("Failed to send reset email"); throw new Error("Failed to send reset email");

View File

@ -1,25 +1,27 @@
import axios from "axios"; import axios from "axios";
import logger from "../config/logger.js"; import logger from "../config/logger.js";
import authFetch from "../utils/authFetch.js";
class SalesforceService { class SalesforceService {
async registerClientInSalesforce(clientData, _id) { async registerClientInSalesforce(clientData, _id) {
try { try {
const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`; const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`;
// Extract only the required data for Salesforce // Extract only the required data for Salesforce
const salesforcePayload = this.prepareSalesforcePayload(clientData, _id); const salesforcePayload = this.prepareSalesforcePayload(clientData, _id);
logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`); logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`);
const response = await axios.post(url, salesforcePayload, { const response = await authFetch(url, {
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(salesforcePayload),
timeout: 30000, // 30 seconds timeout timeout: 30000, // 30 seconds timeout
}); });
logger.info(`SalesforceService - Client registered successfully in Salesforce: ${clientData.userDetails.email}`); logger.info(`SalesforceService - Client registered successfully in Salesforce: ${clientData.userDetails.email}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`); logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`);
@ -46,6 +48,8 @@ class SalesforceService {
userDetails: { userDetails: {
_id: _id, _id: _id,
name: userDetails.name, name: userDetails.name,
firstName: userDetails.firstName,
lastName: userDetails.lastName,
email: userDetails.email.toLowerCase(), email: userDetails.email.toLowerCase(),
address: userDetails.address || null, address: userDetails.address || null,
city: userDetails.city || null, city: userDetails.city || null,

View File

@ -18,7 +18,7 @@ class VaClientService {
} }
// Sanitize and prepare data // Sanitize and prepare data
const clientData = this.sanitizeClientData(data); const clientData = await this.sanitizeClientData(data);
const response = await this.clientRepository.create(clientData); const response = await this.clientRepository.create(clientData);
if(response) { if(response) {
@ -75,7 +75,7 @@ class VaClientService {
// Define allowed fields for update (excluding password) // Define allowed fields for update (excluding password)
const allowedFields = [ const allowedFields = [
"name", "address", "city", "state", "zip", "name", "firstName", "lastName", "address", "city", "state", "zip",
"billingName", "billingEmail", "billingAddress", "billingCity", "billingName", "billingEmail", "billingAddress", "billingCity",
"billingState", "billingZip", "position", "signatoryName", "billingState", "billingZip", "position", "signatoryName",
"signatoryEmail", "paymentMethod", "bankName", "accountNumber", "signatoryEmail", "paymentMethod", "bankName", "accountNumber",
@ -263,13 +263,19 @@ class VaClientService {
} }
// Helper method to sanitize data // Helper method to sanitize data
sanitizeClientData(payload) { async sanitizeClientData(payload) {
const { userDetails } = payload; const { userDetails } = payload;
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
return { return {
userDetails: { userDetails: {
firstName: userDetails.firstName,
lastName: userDetails.lastName,
name: userDetails.name, name: userDetails.name,
email: userDetails.email.toLowerCase(), email: userDetails.email.toLowerCase(),
password: hashedPassword,
address: userDetails.address || null, address: userDetails.address || null,
city: userDetails.city || null, city: userDetails.city || null,
state: userDetails.state || null, state: userDetails.state || null,

68
src/utils/authFetch.js Normal file
View File

@ -0,0 +1,68 @@
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

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

View File

@ -0,0 +1,6 @@
function getDayNumber(date = new Date()) {
const today = new Date(date);
return today.getUTCDay(); // getDay() already returns 0-6 for Sunday-Saturday
}
export default getDayNumber;

173
src/vpnHandler/vpn.js Normal file
View File

@ -0,0 +1,173 @@
import fs from "fs";
import { execSync } from "child_process";
const WG_CONF = "/etc/wireguard/wg0.conf";
const PEER_DB = "/etc/wireguard/peers.json";
const CLIENT_DIR = "/etc/wireguard/clients";
const SERVER_PUBLIC_KEY = process.env.SERVER_PUBLIC_KEY || "jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U=";
const SERVER_IP = process.env.SERVER_PUBLIC_IP || "35.95.209.161";
function loadDb() {
try {
return JSON.parse(fs.readFileSync(PEER_DB, "utf8"));
} catch {
const initial = { lastIp: 1, active: {}, free: [] };
fs.writeFileSync(PEER_DB, JSON.stringify(initial, null, 2));
return initial;
}
}
function saveDb(db) {
fs.writeFileSync(PEER_DB, JSON.stringify(db, null, 2));
}
function allocateIp(db) {
if (db.free.length > 0) {
return db.free.shift(); // reuse old IP
}
db.lastIp++;
if (db.lastIp >= 255) db.lastIp = 2;
return `10.0.0.${db.lastIp}`;
}
function generateKeyPair() {
const privateKey = execSync("wg genkey").toString().trim();
const publicKey = execSync(`echo ${privateKey} | wg pubkey`).toString().trim();
return { privateKey, publicKey };
}
// function addPeerToServer(publicKey, ip) {
// const ipLast = ip.split(".")[3];
// const peerEntry = `
// [Peer]
// PublicKey = ${publicKey}
// AllowedIPs = 10.0.0.${ipLast}/32
// `;
// fs.appendFileSync(WG_CONF, peerEntry);
// // apply config instantly
// // execSync(`wg syncconf wg0 <(wg-quick strip wg0)`, { shell: "/bin/bash" });
// execSync(`wg-quick strip wg0 > /tmp/wg0.tmp`);
// execSync(`wg syncconf wg0 /tmp/wg0.tmp`);
// }
function addPeerToServer(publicKey, ip) {
const peerEntry = `
[Peer]
PublicKey = ${publicKey}
AllowedIPs = ${ip}/32
`;
fs.appendFileSync(WG_CONF, peerEntry);
execSync("wg-quick strip wg0 > /tmp/wg0.tmp");
execSync("wg syncconf wg0 /tmp/wg0.tmp");
execSync("rm -f /tmp/wg0.tmp");
execSync(
`wg set wg0 peer ${publicKey} allowed-ips ${ip}/32`,
{ stdio: "inherit" }
);
}
function generateClientConf(privateKey, ip) {
return `
[Interface]
PrivateKey = ${privateKey}
Address = ${ip}/32
DNS = 1.1.1.1
[Peer]
PublicKey = ${SERVER_PUBLIC_KEY}
Endpoint = ${SERVER_IP}:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
`;
}
export function createWireGuardConnection() {
let db = loadDb();
// 1. allocate IP
const ip = allocateIp(db);
// 2. generate keys
const { privateKey, publicKey } = generateKeyPair();
// 3. update server config
addPeerToServer(publicKey, ip);
// 4. update DB
db.active[ip] = publicKey;
saveDb(db);
// 5. prepare client.conf
const clientConf = generateClientConf(privateKey, ip);
// (optional) save it
if (!fs.existsSync(CLIENT_DIR)) fs.mkdirSync(CLIENT_DIR);
fs.writeFileSync(`${CLIENT_DIR}/client-${ip}.conf`, clientConf);
// 6. return to Electron app
return { ip, publicKey, clientConf };
}
// -------- Disconnect VPN PART START -----------
const removePeerFromConf = (publicKey) => {
const conf = fs.readFileSync(WG_CONF, "utf8");
const peerRegex = new RegExp(
`\\n\\[Peer\\][\\s\\S]*?PublicKey = ${publicKey}[\\s\\S]*?(?=\\n\\[Peer\\]|$)`,
"g"
);
const updated = conf.replace(peerRegex, "\n");
fs.writeFileSync(WG_CONF, updated);
}
function syncWireGuard() {
execSync("wg-quick strip wg0 > /tmp/wg0.tmp");
execSync("wg syncconf wg0 /tmp/wg0.tmp");
execSync("rm -f /tmp/wg0.tmp");
}
export function disconnectWireGuardConnection(ip) {
const db = loadDb();
const publicKey = db.active[ip];
if (!db.active[ip]) {
return { success: false, message: "VPN already disconnected" };
}
// 1️⃣ Remove peer from live kernel
execSync(`wg set wg0 peer ${publicKey} remove`);
// 2️⃣ Remove from config file
removePeerFromConf(publicKey);
// 3️⃣ Sync config
syncWireGuard();
// 4️⃣ Update DB (free IP)
delete db.active[ip];
db.free.push(ip);
saveDb(db);
return {
success: true,
message: "VPN disconnected",
ip
};
}

View File

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

View File

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

View File

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

View File

@ -282,12 +282,12 @@
} }
if (data.success) { if (data.success) {
// 🟢 Save token if(data.userType === "CLIENT") {
localStorage.setItem("knoptpq", data.accessToken); localStorage.setItem("knoptpq", data.accessToken);
// localStorage.setItem("refreshToken", data.refreshToken); window.location.href = "/client/dashboard";
} else {
// 🟢 Redirect window.location.href = "/download-tracker";
window.location.href = "/client/dashboard"; }
} else { } else {
alert("No token received. Please try again."); alert("No token received. Please try again.");
} }

View File

@ -0,0 +1,52 @@
<!-- 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,4 +1,3 @@
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
@ -36,8 +35,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Virtual Assistants List --> <!-- Virtual Assistants List -->
<div class="content-section"> <div class="content-section">
<div class="section-header"> <div class="section-header">
@ -70,3 +67,10 @@
</div> </div>
</div> </div>
</div> </div>
<script>
const tnyk = "<%= typeof sf_token !== 'undefined' ? sf_token : '' %>";
if (tnyk) {
localStorage.setItem('knoptpq', tnyk);
}
</script>

View File

@ -23,7 +23,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Virtual Assistants List --> <!-- Virtual Assistants List -->
<div class="content-section"> <div class="content-section">
<div class="section-header" style="justify-content: space-between;align-items: center;"> <div class="section-header" style="justify-content: space-between;align-items: center;">

View File

@ -55,6 +55,10 @@
<!-- Password Change Section --> <!-- Password Change Section -->
<div class="profile-section"> <div class="profile-section">
<div class="password-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"> <button class="change-password-btn" id="changePasswordBtn">
<i class="fas fa-lock"></i> <i class="fas fa-lock"></i>
Change Password Change Password

View File

@ -0,0 +1,25 @@
<!-- 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

@ -22,7 +22,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-info"> <div class="stat-info">
@ -35,7 +34,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Virtual Assistants List --> <!-- Virtual Assistants List -->
<div class="content-section"> <div class="content-section">
<div class="section-header" style="justify-content: space-between;align-items: center;"> <div class="section-header" style="justify-content: space-between;align-items: center;">

View File

@ -171,12 +171,20 @@
// -------- Download -------- // -------- Download --------
downloadBtn.addEventListener("click", () => { downloadBtn.addEventListener("click", () => {
const activeBtn = document.querySelector(".os-btn.active")
const os = activeBtn.dataset.os;
const link = document.createElement("a"); const link = document.createElement("a");
link.href = `/public/builds/build-1.1.1.1/windows/VA Tracker-Setup-1.1.1.exe`;
link.setAttribute("download", "");
link.click()
if(os === "mac") {
link.href = `/public/builds/build-1.1.1.1/mac/VA Tracker-1.1.1-universal.dmg`;
link.setAttribute("download", "");
} else if (os === "windows") {
link.href = `/public/builds/build-1.1.1.1/windows/VA Tracker-Setup-1.1.1.exe`;
link.setAttribute("download", "");
}
link.click()
// window.location.href = ``; // window.location.href = ``;
}); });
</script> </script>

View File

@ -1,6 +1,8 @@
<div class="container"> <div class="container">
<h2>DocTracker</h2> <div class="logo">
<img id="logo-img" src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
</div>
<form class="card"> <form class="card">
<h2>Reset your password</h2> <h2>Reset your password</h2>
<input type="email" placeholder="Enter your email address*" required /> <input type="email" placeholder="Enter your email address*" required />
@ -10,6 +12,24 @@
</div> </div>
<script> <script>
const root = document.documentElement;
function setTheme(theme) {
document.getElementById("logo-img").src =
theme === "dark"
? "/public/images/logo-white-vertical.svg"
: "/public/images/logo-colored-vertical.svg";
root.setAttribute("data-theme", theme);
}
function getSystemTheme() {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
setTheme(getSystemTheme());
function validateEmail(email) { function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/; const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return emailRegex.test(email); return emailRegex.test(email);

View File

@ -12,7 +12,6 @@
<body> <body>
<%- body %> <%- body %>
</body> </body>
</html> </html>

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title> <title>
<%= title || 'Dashboard' %> <%= title || 'Dashboard' %>
</title> </title>
@ -33,6 +34,8 @@
<%- include('../partials/password-modal') %> <%- include('../partials/password-modal') %>
<%- include('../partials/bankDetails-modal') %>
<!-- COMMON SIDEBAR + LOGOUT --> <!-- COMMON SIDEBAR + LOGOUT -->
<script src="/public/js/common/sidebar.js"></script> <script src="/public/js/common/sidebar.js"></script>
<script src="/public/js/common/header.js"></script> <script src="/public/js/common/header.js"></script>

View File

@ -4,6 +4,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
<title>DocVA | Reset Password</title> <title>DocVA | Reset Password</title>
<style> <style>
@ -112,6 +113,17 @@
opacity: 1; opacity: 1;
text-decoration: underline; text-decoration: underline;
} }
.logo {
text-align: center;
margin-bottom: 20px;
margin-inline: auto;
}
.logo img {
width: 170px;
height: auto;
}
</style> </style>
</head> </head>

View File

@ -4,6 +4,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
<title>Client Registration</title> <title>Client Registration</title>
<link href="/public/css/va-register.css" rel="stylesheet"> <link href="/public/css/va-register.css" rel="stylesheet">

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
<title>Virtual Assistant Registration</title> <title>Virtual Assistant Registration</title>
<link href="/public/css/va-register.css" rel="stylesheet"> <link href="/public/css/va-register.css" rel="stylesheet">

View File

@ -0,0 +1,105 @@
<div id="bankDetailsModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Update Bank Details</h2>
<button class="modal-close" onclick="closeBankDetailsModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<form id="payment-form" class="modal-form">
<div class="payment-option" onclick="selectPaymentOption('card')">
<h4>Credit or Debit Card</h4>
<p>Pay securely with your card</p>
</div>
<div id="card-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="card-number" class="input-field" placeholder=" " required>
<label for="card-number" class="input-label">Card Number *</label>
</div>
</div>
<div class="row">
<div class="form-group">
<div class="input-container">
<input type="text" id="expiry-date" class="input-field" placeholder=" " required>
<label for="expiry-date" class="input-label">Expiry Date (MM/YY) *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="security-code" class="input-field" placeholder=" " oninput="validateCVVVV(this)" onkeypress="return preventInvalidCVVChars(event)" maxlength="4" required>
<label for="security-code" class="input-label">Security Code *</label>
</div>
<div class="invalid-feedback" id="security-code-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid security code
</div>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="card-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="card-zip" class="input-label">ZIP Code *</label>
</div>
<div class="invalid-feedback" id="card-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div>
</div>
<div class="payment-option" onclick="selectPaymentOption('bank')">
<h4>Bank Account (ACH)</h4>
<p>Direct bank transfer</p>
</div>
<div id="bank-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="bank-name" class="input-field" placeholder=" " oninput="validateBankName(this)" onkeypress="return preventInvalidBankNameChars(event)" required>
<label for="bank-name" class="input-label">Bank Name *</label>
</div>
<div class="invalid-feedback" id="bank-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Bank name should contain letters and cannot be only numbers or special characters
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="account-number" class="input-field" placeholder=" " oninput="validateAccountNumber(this)" onkeypress="return preventInvalidAccountNumberChars(event)" required>
<label for="account-number" class="input-label">Account Number *</label>
</div>
<div class="invalid-feedback" id="account-number-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid account number
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="routing-number" class="input-field" placeholder=" " required>
<label for="routing-number" class="input-label">Routing Number *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<select id="account-type" class="select-field" required>
<option value="" disable selected style="display: none"></option>
<option value="checking">Checking</option>
<option value="savings">Savings</option>
</select>
<label for="account-type" class="input-label">Account Type *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="text" id="account-holder-name" class="input-field" placeholder=" " oninput="validateAccountHolderName(this)" onkeypress="return preventInvalidAccountHolderChars(event)" required>
<label for="account-holder-name" class="input-label">Account Holder Name *</label>
</div>
<div class="invalid-feedback" id="account-holder-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account holder name should only contain letters and spaces
</div>
</div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closeBankDetailsModal()">Cancel</button>
<button type="submit" class="save-btn" id="saveBankDetailsBtn">Update Bank Details</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -1,21 +1,21 @@
<div id="logoutModal" class="modal-overlay"> <div id="logoutModal" class="modal-overlay">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">Confirm Logout</h2> <h2 class="modal-title">Confirm Logout</h2>
<button class="modal-close" onclick="closeLogoutModal()"> <button class="modal-close" onclick="closeLogoutModal()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="logout-modal-body"> <div class="logout-modal-body">
<div class="logout-icon"> <div class="logout-icon">
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt"></i>
</div> </div>
<h3 class="logout-title">Are you sure you want to logout?</h3> <h3 class="logout-title">Are you sure you want to logout?</h3>
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p> <p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
<div class="logout-actions"> <div class="logout-actions">
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button> <button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button> <button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
</div>
</div>
</div> </div>
</div> </div>
</div>
</div>

View File

@ -1,33 +1,33 @@
<div id="passwordModal" class="modal-overlay"> <div id="passwordModal" class="modal-overlay">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">Change Password</h2> <h2 class="modal-title">Change Password</h2>
<button class="modal-close" onclick="closePasswordModal()"> <button class="modal-close" onclick="closePasswordModal()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="modal-form" id="passwordForm"> <form class="modal-form" id="passwordForm">
<div class="form-group"> <div class="form-group">
<label class="form-label">Current Password</label> <label class="form-label">Current Password</label>
<input type="password" class="form-input" id="currentPassword" required> <input type="password" class="form-input" id="currentPassword" required>
<div class="error-message" id="currentPasswordError"></div> <div class="error-message" id="currentPasswordError"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">New Password</label> <label class="form-label">New Password</label>
<input type="password" class="form-input" id="newPassword" required> <input type="password" class="form-input" id="newPassword" required>
<div class="error-message" id="newPasswordError"></div> <div class="error-message" id="newPasswordError"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Confirm New Password</label> <label class="form-label">Confirm New Password</label>
<input type="password" class="form-input" id="confirmPassword" required> <input type="password" class="form-input" id="confirmPassword" required>
<div class="error-message" id="confirmPasswordError"></div> <div class="error-message" id="confirmPasswordError"></div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closePasswordModal()">Cancel</button> <button type="button" class="cancel-btn" onclick="closePasswordModal()">Cancel</button>
<button type="submit" class="save-btn" id="savePasswordBtn">Change Password</button> <button type="submit" class="save-btn" id="savePasswordBtn">Change Password</button>
</div> </div>
</form> </form>
</div>
</div> </div>
</div> </div>
</div>

View File

@ -1,39 +1,39 @@
<div id="projectModal" class="modal-overlay"> <div id="projectModal" class="modal-overlay">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;"> <div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
<h2 class="modal-title" id="modalProjectName">Project Details</h2> <h2 class="modal-title" id="modalProjectName">Project Details</h2>
<button class="modal-close" id="modalClose"> <button class="modal-close" id="modalClose">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="va-details-section"> <div class="va-details-section">
<h3 class="va-section-title"> <h3 class="va-section-title">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
Virtual Assistants Virtual Assistants
<span class="va-count-badge" id="vaCountBadge">0</span> <span class="va-count-badge" id="vaCountBadge">0</span>
</h3> </h3>
<div class="va-table-container"> <div class="va-table-container">
<table class="va-table"> <table class="va-table">
<thead> <thead>
<tr> <tr>
<th>Virtual Assistant</th> <th>Virtual Assistant</th>
<th>Status</th> <th>Status</th>
<th>Activity</th> <th>Activity</th>
</tr> </tr>
</thead> </thead>
<tbody id="modalVATableBody"> <tbody id="modalVATableBody">
<!-- VA details will be loaded here --> <!-- VA details will be loaded here -->
</tbody> </tbody>
</table> </table>
<div id="modalNoVAs" class="no-vas-message" style="display: none;"> <div id="modalNoVAs" class="no-vas-message" style="display: none;">
<i class="fas fa-user-friends"></i> <i class="fas fa-user-friends"></i>
<h4>No Virtual Assistants</h4> <h4>No Virtual Assistants</h4>
<p>No VAs are currently assigned to this project.</p> <p>No VAs are currently assigned to this project.</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>

View File

@ -25,7 +25,7 @@
</li> </li>
<li> <li>
<a href="/client/projects" class="sidebar-link"> <a href="/client/projects" class="sidebar-link">
<i class="fas fa-file-invoice"></i> <i class="fas fa-file-invoice"></i>
<span>Projects</span> <span>Projects</span>
@ -33,6 +33,13 @@
</li> </li>
<li>
<a href="/client/credits" class="sidebar-link">
<i class="fas fa-wallet"></i>
<span>Credits</span>
</a>
</li>
<li> <li>
<a href="javascript:void(0)" id="logoutButton" class="sidebar-link logout"> <a href="javascript:void(0)" id="logoutButton" class="sidebar-link logout">

View File

@ -1,36 +1,36 @@
<div id="tipModal" class="modal-overlay"> <div id="tipModal" class="modal-overlay">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">Tip Virtual Assistant</h2> <h2 class="modal-title">Tip Virtual Assistant</h2>
<button class="modal-close" onclick="closeTipModal()"> <button class="modal-close" onclick="closeTipModal()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div>
<div class="modal-body">
<form class="modal-form" id="tipForm">
<div class="form-group">
<label class="form-label">Select Virtual Assistant</label>
<select name="vaForTip" class="form-input" id="vaForTip" required>
<option value="">-- Select Virtual Assistant --</option>
<!-- Options will be populated dynamically -->
</select>
<div class="error-message" id="vaForTipError"></div>
</div>
<div class="form-group">
<label class="form-label">Tip Amount</label>
<input type="text" class="form-input" id="vaTipAmount" required>
<div class="error-message" id="vaTipAmountError"></div>
</div>
<div class="form-group">
<label class="form-label">Tip Description</label>
<textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
<div class="error-message" id="vaTipDescriptionError"></div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
</div>
</form>
</div>
</div>
</div> </div>
<div class="modal-body">
<form class="modal-form" id="tipForm">
<div class="form-group">
<label class="form-label">Select Virtual Assistant</label>
<select name="vaForTip" class="form-input" id="vaForTip" required>
<option value="">-- Select Virtual Assistant --</option>
<!-- Options will be populated dynamically -->
</select>
<div class="error-message" id="vaForTipError"></div>
</div>
<div class="form-group">
<label class="form-label">Tip Amount</label>
<input type="text" class="form-input" id="vaTipAmount" required>
<div class="error-message" id="vaTipAmountError"></div>
</div>
<div class="form-group">
<label class="form-label">Tip Description</label>
<textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
<div class="error-message" id="vaTipDescriptionError"></div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -86,14 +86,20 @@
<form id="account-form"> <form id="account-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="company-name" class="input-field" placeholder=" " <input type="text" id="first-name" class="input-field" placeholder=" " oninput="validateFirstName(this)" required>
oninput="validateCompanyName(this)" required> <label for="first-name" class="input-label">First Name *</label>
<label for="company-name" class="input-label">Company / Client Name *</label>
</div> </div>
<div class="invalid-feedback" id="company-name-error" <div class="invalid-feedback" id="first-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;"> First name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
Company name should only contain letters, numbers, spaces, hyphens, apostrophes, </div>
ampersands, and periods </div>
<div class="form-group">
<div class="input-container">
<input type="text" id="last-name" class="input-field" placeholder=" " oninput="validateLastName(this)" required>
<label for="last-name" class="input-label">Last Name *</label>
</div>
<div class="invalid-feedback" id="last-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Last name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -103,6 +109,24 @@
<label for="email" class="input-label">Email *</label> <label for="email" class="input-label">Email *</label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="password" class="input-label">Create Password *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="confirmPassword" class="input-label">Confirm Password *</label>
</div>
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
<div class="password-actions">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
</div>
</div>
<div class="form-actions"> <div class="form-actions">
<div></div> <!-- Empty div for spacing --> <div></div> <!-- Empty div for spacing -->
<button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
@ -117,6 +141,15 @@
<div class="error-message" id="step-2-error-message"></div> <div class="error-message" id="step-2-error-message"></div>
</div> </div>
<form id="business-form"> <form id="business-form">
<div class="form-group">
<div class="input-container">
<input type="text" id="company-name" class="input-field" placeholder=" " oninput="validateCompanyName(this)" required>
<label for="company-name" class="input-label">Company Name *</label>
</div>
<div class="invalid-feedback" id="company-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Company name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="address" class="input-field" placeholder=" " required> <input type="text" id="address" class="input-field" placeholder=" " required>
@ -246,8 +279,7 @@
<div class="error-message" id="step-4-error-message"></div> <div class="error-message" id="step-4-error-message"></div>
</div> </div>
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;"> <p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
We will use the details below in the agreement that we will send to your prior to entering the We will use the details below in the agreement that we will send to your prior to starting the project.
project.
</p> </p>
<form id="signatory-form"> <form id="signatory-form">
<div class="form-group"> <div class="form-group">
@ -341,7 +373,7 @@
</div> </div>
</div> </div>
<div class="payment-option" onclick="selectPaymentOption('bank')"> <div class="payment-option" onclick="selectPaymentOption('bank')">
<h4>Bank Account (ACR)</h4> <h4>Bank Account (ACH)</h4>
<p>Direct bank transfer</p> <p>Direct bank transfer</p>
</div> </div>
<div id="bank-details" class="payment-details"> <div id="bank-details" class="payment-details">

View File

@ -241,22 +241,6 @@
</div> </div>
<form id="payment-form"> <form id="payment-form">
<div class="form-group"> <div class="form-group">
<!-- <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
</div>
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" onkeypress="return event.charCode >= 48 && event.charCode <= 57" required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
</div>
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 5-20 characters
</div>
</div>
</div> -->
<div class="payment-option" onclick="selectPaymentMethod('veem')"> <div class="payment-option" onclick="selectPaymentMethod('veem')">
<h4>Veem</h4> <h4>Veem</h4>
<p>Receive payments via Veem</p> <p>Receive payments via Veem</p>
@ -264,11 +248,11 @@
<div id="veem-details" class="payment-details"> <div id="veem-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="veemAccount" class="input-field" placeholder=" " minlength="8" maxlength="25" oninput="validateVeemAccount(this)" onkeypress="return event.charCode >= 48 && event.charCode <= 57" required> <input type="email" id="veemAccount" class="input-field" placeholder=" " oninput="validateVeemEmail(this)" required>
<label for="veemAccount" class="input-label">Veem Account Number *</label> <label for="veemAccount" class="input-label">Veem Email Address *</label>
</div> </div>
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;"> <div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 8-25 characters Invalid Veem email address
</div> </div>
</div> </div>
</div> </div>
@ -288,6 +272,23 @@
</div> </div>
</div> </div>
</div> </div>
<h2 class="form-title additional-info-heading">Additional Information</h2>
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
</div>
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="email" id="payoneerAccount" class="input-field" placeholder=" " oninput="validatePayoneerEmail(this)" required>
<label for="payoneerAccount" class="input-label">Payoneer Email Address *</label>
</div>
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid email address for Payoneer
</div>
</div>
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">