Compare commits
81 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
4e782e980c | |
|
|
bfcd64cf0e | |
|
|
bc3b81e12f | |
|
|
41f5eff4d7 | |
|
|
4d1abaf9aa | |
|
|
0c14e26937 | |
|
|
68a248c25a | |
|
|
221e867a21 | |
|
|
39f415b685 | |
|
|
d4cdc94bbd | |
|
|
2d61f25d5b | |
|
|
99d60185a9 | |
|
|
da3deac57b | |
|
|
0b0f1d5ab3 | |
|
|
b120f75c40 | |
|
|
9f8c76cabd | |
|
|
5822e67296 | |
|
|
412511afec | |
|
|
b3bb9697e8 | |
|
|
90438ea937 | |
|
|
fd9bf9e517 | |
|
|
33779a6eb8 | |
|
|
44132c351e | |
|
|
79baf21819 | |
|
|
6cd02b0f85 | |
|
|
344d6181fa | |
|
|
d036a71810 | |
|
|
a5f8e7bc7f | |
|
|
60be1533bc | |
|
|
dad88d02bb | |
|
|
46f2d7377f | |
|
|
b9f2eb10b4 | |
|
|
5f043ed80a | |
|
|
efa6ce929e | |
|
|
b64962738a | |
|
|
3d337780da | |
|
|
2e5fed545f | |
|
|
dba68ed584 | |
|
|
320d44f4b5 | |
|
|
166cf4db45 | |
|
|
085e0c0e6d | |
|
|
15bbbd8a98 | |
|
|
c20db85ada | |
|
|
0e8ce1f850 | |
|
|
9e4d118009 | |
|
|
668c81b86e | |
|
|
b4b0e27ecf | |
|
|
c74f792df6 | |
|
|
f538371116 | |
|
|
fd382ccb93 | |
|
|
287d078b49 | |
|
|
e79383c1cf | |
|
|
67af7df83c | |
|
|
cbfa03e57c | |
|
|
645adea832 | |
|
|
03814c6c99 | |
|
|
4ed62b25e8 | |
|
|
2e7731e704 | |
|
|
9a811e6f00 | |
|
|
fc821c70ca | |
|
|
6fd3da0837 | |
|
|
140b669bdb | |
|
|
0ef8d60060 | |
|
|
bc98edcb4b | |
|
|
00cb793a7c | |
|
|
59561f211d | |
|
|
4d617d3113 | |
|
|
15f0f3d782 | |
|
|
7ac50fe720 | |
|
|
b085a88579 | |
|
|
5ee97a7d44 | |
|
|
c04264f054 | |
|
|
a7d770498c | |
|
|
2ca5146846 | |
|
|
6c8e85336c | |
|
|
0c578d8121 | |
|
|
d0bb922da1 | |
|
|
1baa6da4c7 | |
|
|
3154a7e0d3 | |
|
|
142f4f77cd | |
|
|
c7f44c7754 |
|
|
@ -41,7 +41,8 @@
|
|||
"uglify-js": "^3.19.3",
|
||||
"uuid": "^11.0.3",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"wireguard-tools": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.26.4",
|
||||
|
|
@ -149,7 +150,6 @@
|
|||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.26.0",
|
||||
|
|
@ -2361,7 +2361,6 @@
|
|||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
|
@ -2906,7 +2905,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
|
|
@ -3744,8 +3742,7 @@
|
|||
"version": "0.0.1452169",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
|
||||
"integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "0.1.1",
|
||||
|
|
@ -6208,6 +6205,12 @@
|
|||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.mergewith": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
|
|
@ -7996,7 +7999,6 @@
|
|||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -8380,7 +8382,6 @@
|
|||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
|
||||
"integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
|
|
@ -9089,7 +9090,6 @@
|
|||
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
|
|
@ -9137,7 +9137,6 @@
|
|||
"integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.6.1",
|
||||
"@webpack-cli/configtest": "^3.0.1",
|
||||
|
|
@ -9303,7 +9302,6 @@
|
|||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
|
|
@ -9353,6 +9351,15 @@
|
|||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wireguard-tools": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wireguard-tools/-/wireguard-tools-0.1.0.tgz",
|
||||
"integrity": "sha512-oBnnSB6wXGEAB1ZVPfbfpX8WlHNw7wv45r13ILX73felVnrGpMxMjFcKGAsMeRfBOGzXcW0LgAWe+T53dRcI+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.mergewith": "^4.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"start": "nodemon --inspect src/server.js",
|
||||
"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",
|
||||
"dev": "npx webpack --mode development"
|
||||
},
|
||||
|
|
@ -46,7 +46,8 @@
|
|||
"uglify-js": "^3.19.3",
|
||||
"uuid": "^11.0.3",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"wireguard-tools": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.26.4",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-card: #ffffff;
|
||||
|
|
@ -165,7 +165,6 @@ body {
|
|||
}
|
||||
|
||||
.mobile-menu-button {
|
||||
display: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
align-items: center;
|
||||
|
|
@ -201,6 +200,9 @@ body {
|
|||
.dashboard-sidebar {
|
||||
display: block;
|
||||
}
|
||||
.mobile-menu-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -217,6 +219,10 @@ body {
|
|||
.dashboard-sidebar.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
|
|
@ -308,7 +314,6 @@ select,
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
[data-theme="dark"] .sidebar-link:hover {
|
||||
background-color: var(--color-gray-800);
|
||||
}
|
||||
|
|
@ -626,7 +631,7 @@ select,
|
|||
.data-table tbody tr {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
/* cursor: pointer; */
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
|
|
@ -994,17 +999,17 @@ tr[data-vaid]:hover {
|
|||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.project-time {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
.detail-item span {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -1264,6 +1269,7 @@ tr[data-vaid]:hover {
|
|||
background: var(--color-gray-400);
|
||||
}
|
||||
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
|
|
@ -1673,11 +1679,11 @@ tr[data-vaid]:hover {
|
|||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.logout-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -1695,39 +1701,39 @@ tr[data-vaid]:hover {
|
|||
h6 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
|
||||
p,
|
||||
span,
|
||||
div {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
|
||||
/* Form elements */
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-600);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-gray-500);
|
||||
}
|
||||
|
|
@ -1776,7 +1782,6 @@ tr[data-vaid]:hover {
|
|||
transform: scale(1);
|
||||
}
|
||||
|
||||
|
||||
/* ================================
|
||||
PROFILE DROPDOWN (GLASS STYLE)
|
||||
================================ */
|
||||
|
|
@ -1930,11 +1935,294 @@ tr[data-vaid]:hover {
|
|||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
RESPONSIVE
|
||||
================================ */
|
||||
@media (max-width: 768px) {
|
||||
.profile-name {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -670,6 +670,13 @@ body {
|
|||
background-color: rgba(220, 38, 38, 0.1) !important;
|
||||
}
|
||||
|
||||
.additional-info-heading {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
|
|
|
|||
|
|
@ -26,4 +26,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
profileMenu.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const mobileMenuButton = document.querySelector(".mobile-menu-button");
|
||||
const sidebar = document.querySelector(".dashboard-sidebar");
|
||||
mobileMenuButton.addEventListener("click", () => {
|
||||
sidebar.classList.toggle("active");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -94,7 +94,7 @@ function createVATableRow(va) {
|
|||
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
|
||||
const workedTime = formatTime(va.currentSessionDuration);
|
||||
const projectsAssigned = va.projectsAssigned || 0;
|
||||
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : '<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 `
|
||||
<tr class="va-row" data-va-id="${va.id}">
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
@ -491,10 +491,13 @@ async function validateStep(step) {
|
|||
}
|
||||
} else if (step === 1) {
|
||||
const email = document.getElementById('email').value;
|
||||
const companyName = document.getElementById('company-name').value;
|
||||
if (!validateEmail(email) && !companyName) {
|
||||
const firstName = document.getElementById('first-name').value;
|
||||
const lastName = document.getElementById('last-name').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
if (!firstName && !lastName && !password && !confirmPassword) {
|
||||
isValid = false;
|
||||
showError('Both Fields are Required', null, step);
|
||||
showError('Fields are Required', null, step);
|
||||
let requiredFields1 = form.querySelectorAll('[required]');
|
||||
requiredFields1.forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
|
|
@ -516,9 +519,48 @@ async function validateStep(step) {
|
|||
isValid = false;
|
||||
}
|
||||
}
|
||||
if (!firstName) {
|
||||
isValid = false;
|
||||
showError('First Name is Required', 'first-name', step);
|
||||
return isValid;
|
||||
}
|
||||
if (!lastName) {
|
||||
isValid = false;
|
||||
showError('Last Name is Required', 'last-name', step);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
showError('Password must be at least 8 characters long', 'password', step);
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
document.getElementById('password-match-error').style.display = 'block';
|
||||
document.getElementById('confirmPassword').classList.add('field-error');
|
||||
showError('Passwords do not match', 'confirmPassword', step);
|
||||
isValid = false;
|
||||
} else {
|
||||
document.getElementById('password-match-error').style.display = 'none';
|
||||
document.getElementById('confirmPassword').classList.remove('field-error');
|
||||
}
|
||||
|
||||
// Validate other required fields
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
requiredFields.forEach(field => {
|
||||
if (field.id !== 'email' && !field.value.trim()) {
|
||||
isValid = false;
|
||||
field.classList.add('field-error');
|
||||
} else if (field.id !== 'email') {
|
||||
field.classList.remove('field-error');
|
||||
}
|
||||
});
|
||||
} else if (step === 2) {
|
||||
const companyName = document.getElementById('company-name').value;
|
||||
|
||||
if (!companyName) {
|
||||
isValid = false;
|
||||
showError('Company / Client Name is Required', null, step);
|
||||
showError('Company Name is Required', 'company-name', step);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
|
@ -610,10 +652,13 @@ function getFormId(step) {
|
|||
function saveStepData(step) {
|
||||
switch(step) {
|
||||
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.password = document.getElementById('password').value;
|
||||
break;
|
||||
case 2:
|
||||
formData.userDetails.name = document.getElementById('company-name').value;
|
||||
formData.userDetails.address = document.getElementById('address').value;
|
||||
formData.userDetails.city = document.getElementById('city').value;
|
||||
formData.userDetails.state = document.getElementById('state').value;
|
||||
|
|
@ -767,7 +812,7 @@ async function submitForm() {
|
|||
nextStep(6);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '/client/login';
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
// Handle server errors
|
||||
|
|
@ -857,28 +902,29 @@ async function detectLocation() {
|
|||
await populateStates(countryCode, 'state');
|
||||
await populateStates(countryCode, 'billing-state');
|
||||
|
||||
const stateSelect = document.getElementById('state');
|
||||
const billingStateSelect = document.getElementById('billing-state');
|
||||
// auto populate state if region matches
|
||||
// const stateSelect = document.getElementById('state');
|
||||
// const billingStateSelect = document.getElementById('billing-state');
|
||||
|
||||
if (stateSelect && region) {
|
||||
const matchedState = Array.from(stateSelect.options).find(
|
||||
opt => opt.text.toLowerCase() === region.toLowerCase()
|
||||
);
|
||||
if (matchedState) {
|
||||
stateSelect.value = matchedState.value;
|
||||
stateSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
// if (stateSelect && region) {
|
||||
// const matchedState = Array.from(stateSelect.options).find(
|
||||
// opt => opt.text.toLowerCase() === region.toLowerCase()
|
||||
// );
|
||||
// if (matchedState) {
|
||||
// stateSelect.value = matchedState.value;
|
||||
// stateSelect.dispatchEvent(new Event('change'));
|
||||
// }
|
||||
// }
|
||||
|
||||
if (billingStateSelect && region) {
|
||||
const matchedBillingState = Array.from(billingStateSelect.options).find(
|
||||
opt => opt.text.toLowerCase() === region.toLowerCase()
|
||||
);
|
||||
if (matchedBillingState) {
|
||||
billingStateSelect.value = matchedBillingState.value;
|
||||
billingStateSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
// if (billingStateSelect && region) {
|
||||
// const matchedBillingState = Array.from(billingStateSelect.options).find(
|
||||
// opt => opt.text.toLowerCase() === region.toLowerCase()
|
||||
// );
|
||||
// if (matchedBillingState) {
|
||||
// billingStateSelect.value = matchedBillingState.value;
|
||||
// billingStateSelect.dispatchEvent(new Event('change'));
|
||||
// }
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error('Error detecting location:', error);
|
||||
|
||||
|
|
@ -1240,7 +1286,181 @@ function preventNumbers(event) {
|
|||
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) {
|
||||
const errorElement = document.getElementById('company-name-error');
|
||||
const value = input.value;
|
||||
|
|
@ -1295,7 +1515,7 @@ function validateCompanyNameContent(companyName) {
|
|||
}
|
||||
|
||||
// Function to prevent invalid character input in real-time
|
||||
function preventInvalidCompanyChars(event) {
|
||||
function preventInvalidCompanyNameChars(event) {
|
||||
const charCode = event.which ? event.which : event.keyCode;
|
||||
const key = event.key;
|
||||
|
||||
|
|
@ -1954,4 +2174,52 @@ function preventInvalidAccountHolderChars(event) {
|
|||
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Function to generate a strong password
|
||||
function generatePassword() {
|
||||
const length = 12;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
||||
let password = "";
|
||||
|
||||
// Ensure at least one of each required character type
|
||||
password += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)];
|
||||
password += "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)];
|
||||
password += "0123456789"[Math.floor(Math.random() * 10)];
|
||||
password += "!@#$%^&*"[Math.floor(Math.random() * 8)];
|
||||
|
||||
// Fill the rest randomly
|
||||
for (let i = password.length; i < length; i++) {
|
||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
|
||||
// Shuffle the password
|
||||
password = password.split('').sort(() => 0.5 - Math.random()).join('');
|
||||
|
||||
// Set the password field
|
||||
document.getElementById('password').value = password;
|
||||
document.getElementById('confirmPassword').value = password;
|
||||
|
||||
// Trigger input events to update UI
|
||||
document.getElementById('password').dispatchEvent(new Event('input'));
|
||||
document.getElementById('confirmPassword').dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
// Function to toggle password visibility
|
||||
function togglePasswordVisibility() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const confirmField = document.getElementById('confirmPassword');
|
||||
const toggleButton = document.querySelector('.password-toggle');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
confirmField.type = 'text';
|
||||
toggleButton.textContent = 'Hide Password';
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
confirmField.type = 'password';
|
||||
toggleButton.textContent = 'Show Password';
|
||||
}
|
||||
}
|
||||
|
|
@ -617,16 +617,22 @@
|
|||
} else {
|
||||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||
if (paymentMethod === 'payoneer') {
|
||||
// const payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||
// if (!payoneerAccount) {
|
||||
// showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
||||
// isValid = false;
|
||||
// }
|
||||
const payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||
if (!payoneerAccount) {
|
||||
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
||||
isValid = false;
|
||||
} else if (!validateEmail(payoneerAccount)) {
|
||||
showError('Please enter a valid email address for Payoneer', 'payoneerAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
} else if (paymentMethod === 'veem') {
|
||||
const veemAccount = document.getElementById('veemAccount').value;
|
||||
if (!veemAccount) {
|
||||
showError('Please enter your Veem account number', 'veemAccount', step);
|
||||
isValid = false;
|
||||
} else if (!validateEmail(veemAccount)) {
|
||||
showError('Please enter a valid email address for Veem', 'veemAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
} else if (paymentMethod === 'wise') {
|
||||
const wiseEmail = document.getElementById('wiseEmail').value;
|
||||
|
|
@ -703,9 +709,9 @@
|
|||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||
formData.userDetails.paymentMethod = paymentMethod;
|
||||
|
||||
// if (document.getElementById('payoneerAccount').value) {
|
||||
// formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||
// }
|
||||
if (document.getElementById('payoneerAccount').value) {
|
||||
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||
}
|
||||
if (document.getElementById('veemAccount').value) {
|
||||
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
|
||||
}
|
||||
|
|
@ -734,7 +740,7 @@
|
|||
|
||||
// Show appropriate payment details
|
||||
if (method === 'payoneer') {
|
||||
// document.getElementById('payoneer-details').style.display = 'block';
|
||||
document.getElementById('payoneer-details').style.display = 'block';
|
||||
} else if (method === 'veem') {
|
||||
document.getElementById('veem-details').style.display = 'block';
|
||||
} else if (method === 'wise') {
|
||||
|
|
@ -763,6 +769,48 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function validateVeemEmail(input) {
|
||||
const wiseError = document.getElementById('veem-error');
|
||||
const value = input.value.trim();
|
||||
|
||||
// Remove any validation styling first
|
||||
input.style.border = '';
|
||||
wiseError.style.display = 'none';
|
||||
|
||||
if (value.length > 0) {
|
||||
// Check if it's a valid email
|
||||
if (!validateEmail(value)) {
|
||||
wiseError.textContent = 'Please enter a valid email address for Veem';
|
||||
wiseError.style.display = 'block';
|
||||
input.style.border = '1px solid #ef4444';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validatePayoneerEmail(input) {
|
||||
const wiseError = document.getElementById('payoneer-error');
|
||||
const value = input.value.trim();
|
||||
|
||||
// Remove any validation styling first
|
||||
input.style.border = '';
|
||||
wiseError.style.display = 'none';
|
||||
|
||||
if (value.length > 0) {
|
||||
// Check if it's a valid email
|
||||
if (!validateEmail(value)) {
|
||||
wiseError.textContent = 'Please enter a valid email address for Payoneer';
|
||||
wiseError.style.display = 'block';
|
||||
input.style.border = '1px solid #ef4444';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Function to generate a strong password
|
||||
function generatePassword() {
|
||||
const length = 12;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
||||
|
||||
// Getting client dashboard data - all VAs, projects, and active VAs
|
||||
async function client_dashboard(req, res) {
|
||||
|
|
@ -314,9 +315,11 @@ async function client_dashboard(req, res) {
|
|||
};
|
||||
});
|
||||
|
||||
const dayNumber = getDayNumber();
|
||||
|
||||
// Calculate dashboard statistics
|
||||
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
|
||||
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0);
|
||||
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours[dayNumber] || 0), 0);
|
||||
|
||||
const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive);
|
||||
const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
||||
|
||||
function formatHoursToReadable(hours) {
|
||||
const wholeHours = Math.floor(hours);
|
||||
|
|
@ -34,8 +35,6 @@ async function client_dashboard_projects(req, res) {
|
|||
|
||||
// STEP 1: Get client ID from token
|
||||
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
|
||||
|
||||
console.log("Line 38" , clientUser)
|
||||
if (!clientUser) {
|
||||
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 activeVAsCount = validVADetails.filter(va => va.isActive).length;
|
||||
|
||||
const dayNumber = getDayNumber();
|
||||
|
||||
return {
|
||||
id: project._id,
|
||||
name: project.name,
|
||||
hourlyRate: project.hourlyRate,
|
||||
vaAssigned: validVADetails.length,
|
||||
activeVAs: activeVAsCount,
|
||||
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || "0h",
|
||||
dedicatedHours: formatHoursToReadable(project.dedicatedHours[dayNumber]) || "0h",
|
||||
status: project.status || '',
|
||||
assignedVADetails: validVADetails
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,51 +2,67 @@ import VaClientRepository from "../../repository/vaClient.repository.js";
|
|||
import VaClientService from "../../services/vaClient.service.js";
|
||||
import logger from "../../config/logger.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import PasswordResetService from "../../services/passwordReset.service.js";
|
||||
import PasswordResetRepository from "../../repository/passwordReset.repository.js";
|
||||
|
||||
|
||||
const vaClientService = new VaClientService(new VaClientRepository());
|
||||
|
||||
const passwordResetService = new PasswordResetService(
|
||||
new PasswordResetRepository(),
|
||||
// new EmailService({
|
||||
// host: process.env.SMTP_HOST,
|
||||
// port: process.env.SMTP_PORT,
|
||||
// user: process.env.SMTP_USER,
|
||||
// pass: process.env.SMTP_PASS,
|
||||
// from: process.env.EMAIL_FROM,
|
||||
// })
|
||||
);
|
||||
|
||||
async function va_register_client(req, res) {
|
||||
logger.info("ClientController - Register Client Hit ...");
|
||||
|
||||
|
||||
try {
|
||||
const payload = req.body || {};
|
||||
const { userDetails } = payload;
|
||||
if (!userDetails || !userDetails.email || !userDetails.name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Missing required fields: name and email are required"
|
||||
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.lastName || !userDetails.password || !userDetails.name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Missing required fields: firstName, lastName, email, password, and company name are required"
|
||||
});
|
||||
}
|
||||
|
||||
const created = await vaClientService.registerClient(payload);
|
||||
|
||||
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
await passwordResetService.requestPasswordReset(userDetails.email, 'client');
|
||||
logger.info(`Client password reset requested for: ${userDetails.email}`);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: created,
|
||||
message: "Client registered successfully"
|
||||
message: "Client registered successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`ClientController - Register error: ${error.message}`);
|
||||
|
||||
|
||||
if (error.message === "User already exists with this email") {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes("Salesforce API is unreachable")) {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: "Salesforce service is currently unavailable. Please try again later."
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: "Salesforce service is currently unavailable. Please try again later."
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -56,52 +72,52 @@ async function get_client_profile(req, res) {
|
|||
try {
|
||||
const email = req.body.clientEmail;
|
||||
const client = await vaClientService.getClientByEmail(email);
|
||||
|
||||
|
||||
if (!client) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: client
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: client
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`ClientController - Get client error: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function delete_client(req, res) {
|
||||
logger.info("ClientController - Delete Client Hit ...");
|
||||
|
||||
|
||||
try {
|
||||
const { clientId } = req.params;
|
||||
|
||||
if (!clientId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Client ID is required"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Client ID is required"
|
||||
});
|
||||
}
|
||||
|
||||
const deletedClient = await vaClientService.deleteClient(clientId);
|
||||
|
||||
if (!deletedClient) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`ClientController - Client deleted successfully: ${clientId}`);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Client deleted successfully",
|
||||
data: {
|
||||
clientId: clientId,
|
||||
|
|
@ -111,79 +127,79 @@ async function delete_client(req, res) {
|
|||
});
|
||||
} catch (error) {
|
||||
logger.error(`ClientController - Delete error: ${error.message}`);
|
||||
|
||||
|
||||
if (error.message === "Client not found") {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function update_client(req, res) {
|
||||
logger.info("ClientController - Update Client Hit ...");
|
||||
|
||||
|
||||
try {
|
||||
const { clientId } = req.params;
|
||||
const payload = req.body || {};
|
||||
const { userDetails } = payload;
|
||||
|
||||
if (!clientId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Client ID is required"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Client ID is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (!userDetails) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Missing userDetails in request body"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Missing userDetails in request body"
|
||||
});
|
||||
}
|
||||
|
||||
const updatedClient = await vaClientService.updateClient(clientId, payload);
|
||||
|
||||
logger.info(`ClientController - Client updated successfully: ${clientId}`);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Client updated successfully",
|
||||
data: updatedClient
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`ClientController - Update error: ${error.message}`);
|
||||
|
||||
|
||||
if (error.message === "Client not found") {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes("Email already exists")) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function get_client_dashboard_profile(req, res) {
|
||||
try {
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
}
|
||||
|
|
@ -195,14 +211,14 @@ async function get_client_dashboard_profile(req, res) {
|
|||
}
|
||||
|
||||
const clientEmail = decoded.email;
|
||||
|
||||
|
||||
// Use the new method that returns only necessary fields
|
||||
const client = await vaClientService.getClientProfileByEmail(clientEmail);
|
||||
|
||||
|
||||
if (!client) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -217,15 +233,15 @@ async function get_client_dashboard_profile(req, res) {
|
|||
position: client.userDetails?.position || ''
|
||||
};
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: profileData
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: profileData
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`ClientController - Get dashboard profile error: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -234,8 +250,8 @@ async function update_client_password(req, res) {
|
|||
logger.info("ClientController - Update Client Password Hit ...");
|
||||
console.log("Update Client Password - Request Body:", req.body);
|
||||
try {
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
}
|
||||
|
|
@ -249,14 +265,14 @@ async function update_client_password(req, res) {
|
|||
const clientEmail = decoded.email;
|
||||
const payload = req.body || {};
|
||||
const { userDetails } = payload;
|
||||
|
||||
|
||||
// Get client by email to get the ID
|
||||
const client = await vaClientService.getClientByEmail(clientEmail);
|
||||
|
||||
|
||||
if (!client) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Client not found"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -266,41 +282,41 @@ async function update_client_password(req, res) {
|
|||
const result = await vaClientService.updateClientPassword(clientId, userDetails);
|
||||
|
||||
logger.info(`ClientController - Client password updated successfully: ${clientEmail}`);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Password updated successfully",
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`ClientController - Update password error: ${error.message}`);
|
||||
|
||||
|
||||
if (error.message === "Client not found") {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes("Current password is incorrect") ||
|
||||
error.message.includes("New passwords do not match") ||
|
||||
error.message.includes("All password fields are required")) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
error.message.includes("New passwords do not match") ||
|
||||
error.message.includes("All password fields are required")) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
va_register_client,
|
||||
export {
|
||||
va_register_client,
|
||||
get_client_profile,
|
||||
delete_client,
|
||||
update_client,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { ClientModel } from "../../models/vaModel.js";
|
||||
import { ClientModel, VAModel } from "../../models/vaModel.js";
|
||||
|
||||
// Login VA
|
||||
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" });
|
||||
}
|
||||
|
||||
let isVa = false;
|
||||
|
||||
// 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) {
|
||||
return res.status(404).json({ success: false, message: "Invalid credentials" });
|
||||
}
|
||||
|
|
@ -49,6 +58,7 @@ async function va_login_client(req, res) {
|
|||
id: user._id,
|
||||
email: user.userDetails.email,
|
||||
name: user.userDetails.firstName,
|
||||
userType: isVa ? "VA" : "CLIENT",
|
||||
},
|
||||
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
|
||||
{ expiresIn: "30d" }
|
||||
|
|
@ -69,6 +79,7 @@ async function va_login_client(req, res) {
|
|||
name: user.userDetails.firstName,
|
||||
email: user.userDetails.email,
|
||||
},
|
||||
userType: isVa ? "VA" : "CLIENT",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("va_client_login_register error:", error);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ async function delete_project_sf(req, res) {
|
|||
// 2. Remove project from all assigned VAs' projectIds arrays
|
||||
if (vaAssigned.length > 0) {
|
||||
const vaResult = await VAModel.updateMany(
|
||||
{ _id: { $in: vaAssigned } },
|
||||
{ _id: { $in: vaAssigned.vaId } },
|
||||
{ $pull: { projectIds: projectObjectId } }
|
||||
);
|
||||
deletionSummary.vasUpdated = vaResult.modifiedCount;
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
|
|||
// Register a new VA
|
||||
async function project_add_sf(req, res) {
|
||||
try {
|
||||
const payload = req.body|| {};
|
||||
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
|
||||
const payload = req.body || {};
|
||||
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
|
||||
|
||||
// Validate required fields
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project name is required"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project name is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Client ID is required"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Client ID is required"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -27,53 +27,62 @@ async function project_add_sf(req, res) {
|
|||
try {
|
||||
clientObjectId = new Types.ObjectId(clientId);
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid Client ID format"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid Client ID format"
|
||||
});
|
||||
}
|
||||
|
||||
// Validate and convert vaAssigned to array of ObjectIds
|
||||
let vaAssignedArray = [];
|
||||
if (vaAssigned) {
|
||||
if (Array.isArray(vaAssigned)) {
|
||||
try {
|
||||
vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid VA ID format in vaAssigned array"
|
||||
});
|
||||
}
|
||||
} else if (typeof vaAssigned === 'string') {
|
||||
// Handle single VA ID as string
|
||||
try {
|
||||
vaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid VA ID format"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// let vaAssignedArray = [];
|
||||
// if (vaAssigned) {
|
||||
// if (Array.isArray(vaAssigned)) {
|
||||
// try {
|
||||
// vaAssignedArray = vaAssigned.map(vaId => {
|
||||
// if(typeof vaId === 'object' && "isFlexibleAllowed" in vaId){
|
||||
// // Handle Error form SF Side for now - That vaId is coming as object
|
||||
// return new Types.ObjectId(vaId.vaId)
|
||||
// } else {
|
||||
// return new Types.ObjectId(vaId)
|
||||
// }
|
||||
// });
|
||||
// } catch (error) {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: "Invalid VA ID format in vaAssigned array"
|
||||
// });
|
||||
// }
|
||||
// } else if (typeof vaAssigned === 'string') {
|
||||
// // Handle single VA ID as string
|
||||
// try {
|
||||
// vaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
||||
// } catch (error) {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: "Invalid VA ID format"
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// sanitize/construct allowed fields
|
||||
const toCreate = {
|
||||
name: name.trim(),
|
||||
hourlyRate: hourlyRate || '0',
|
||||
clientId: clientObjectId,
|
||||
vaAssigned: vaAssignedArray,
|
||||
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
|
||||
vaAssigned: vaAssigned,
|
||||
dedicatedHours: dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"],
|
||||
vaWorkingScheduleStart: vaWorkingScheduleStart || null,
|
||||
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
|
||||
isFlexibleAllowedPt: isFlexibleAllowedPt || true,
|
||||
isFlexibleAllowed: isFlexibleAllowed || true,
|
||||
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
|
||||
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
|
||||
};
|
||||
|
||||
const created = await ProjectModel.create(toCreate);
|
||||
|
||||
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
|
||||
|
||||
const relationshipUpdate = await updateEntityRelationships({
|
||||
project: {
|
||||
projectId: created._id,
|
||||
|
|
@ -87,7 +96,7 @@ async function project_add_sf(req, res) {
|
|||
}
|
||||
|
||||
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("project_add_sf error:", error);
|
||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||
|
|
|
|||
|
|
@ -10,25 +10,25 @@ async function update_project_sf(req, res) {
|
|||
try {
|
||||
const { projectId } = req.params;
|
||||
const payload = req.body || {};
|
||||
const {
|
||||
name,
|
||||
hourlyRate,
|
||||
vaAssigned,
|
||||
dedicatedHours,
|
||||
const {
|
||||
name,
|
||||
hourlyRate,
|
||||
vaAssigned,
|
||||
dedicatedHours,
|
||||
status,
|
||||
totalLoggedHours ,
|
||||
totalLoggedHours,
|
||||
vaWorkingScheduleStart,
|
||||
vaWorkingScheduleEnd,
|
||||
isFlexibleAllowedPt,
|
||||
isFlexibleAllowed,
|
||||
vaWorkingDelayAllowedStart,
|
||||
vaWorkingDelayAllowedEnd
|
||||
} = payload;
|
||||
|
||||
// Validate projectId
|
||||
if (!projectId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project ID is required"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project ID is required"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -36,18 +36,18 @@ async function update_project_sf(req, res) {
|
|||
try {
|
||||
projectObjectId = new Types.ObjectId(projectId);
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid Project ID format"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid Project ID format"
|
||||
});
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const existingProject = await ProjectModel.findById(projectObjectId);
|
||||
if (!existingProject) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Project not found"
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -59,9 +59,9 @@ async function update_project_sf(req, res) {
|
|||
// Update name if provided
|
||||
if (name !== undefined) {
|
||||
if (!name.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project name cannot be empty"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project name cannot be empty"
|
||||
});
|
||||
}
|
||||
updateData.name = name.trim();
|
||||
|
|
@ -74,7 +74,7 @@ async function update_project_sf(req, res) {
|
|||
|
||||
// Update dedicated hours if provided
|
||||
if (dedicatedHours !== undefined) {
|
||||
updateData.dedicatedHours = parseFloat(dedicatedHours) || 0;
|
||||
updateData.dedicatedHours = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"];
|
||||
}
|
||||
|
||||
// Update total logged hours if provided
|
||||
|
|
@ -85,9 +85,9 @@ async function update_project_sf(req, res) {
|
|||
// Update status if provided
|
||||
if (status !== undefined) {
|
||||
if (!["ACTIVE", "INACTIVE"].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Status must be either ACTIVE or INACTIVE"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Status must be either ACTIVE or INACTIVE"
|
||||
});
|
||||
}
|
||||
updateData.status = status;
|
||||
|
|
@ -102,8 +102,8 @@ async function update_project_sf(req, res) {
|
|||
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
|
||||
}
|
||||
|
||||
if (isFlexibleAllowedPt !== undefined) {
|
||||
updateData.isFlexibleAllowedPt = isFlexibleAllowedPt;
|
||||
if (isFlexibleAllowed !== undefined) {
|
||||
updateData.isFlexibleAllowed = isFlexibleAllowed;
|
||||
}
|
||||
|
||||
if (vaWorkingDelayAllowedStart !== undefined) {
|
||||
|
|
@ -114,41 +114,52 @@ async function update_project_sf(req, res) {
|
|||
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
|
||||
}
|
||||
|
||||
if (vaAssigned !== undefined && vaAssigned !== null && Array.isArray(vaAssigned)) {
|
||||
updateData.vaAssigned = vaAssigned;
|
||||
}
|
||||
|
||||
// Handle VA assignments - this requires relationship updates
|
||||
if (vaAssigned !== undefined) {
|
||||
let newVaAssignedArray = [];
|
||||
let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
|
||||
|
||||
// Parse and validate VA IDs
|
||||
if (vaAssigned && vaAssigned.length > 0) {
|
||||
if (Array.isArray(vaAssigned)) {
|
||||
try {
|
||||
newVaAssignedArray = vaAssigned.map(vaId => 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) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid VA ID format in vaAssigned array"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid VA ID format in vaAssigned array"
|
||||
});
|
||||
}
|
||||
} else if (typeof vaAssigned === 'string') {
|
||||
try {
|
||||
newVaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid VA ID format"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid VA ID format"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateData.vaAssigned = newVaAssignedArray;
|
||||
// updateData.vaAssigned = newVaAssignedArray;
|
||||
|
||||
// Calculate VAs to add and remove for relationship updates
|
||||
const currentVaIds = existingProject.vaAssigned.map(va => va.toString());
|
||||
const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.toString());
|
||||
const newVaIds = newVaAssignedArray.map(va => va.toString());
|
||||
|
||||
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
|
||||
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString()));
|
||||
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.vaId.toString()));
|
||||
|
||||
// Update relationships for added VAs
|
||||
if (vaToAdd.length > 0) {
|
||||
|
|
@ -175,9 +186,9 @@ async function update_project_sf(req, res) {
|
|||
|
||||
// If no fields to update
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No valid fields to update"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No valid fields to update"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +198,7 @@ async function update_project_sf(req, res) {
|
|||
updateData,
|
||||
{ new: true, runValidators: true }
|
||||
).populate('clientId', 'userDetails.name userDetails.email')
|
||||
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
|
||||
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
|
||||
|
||||
// If VA assignments were changed but relationship update failed, log warning
|
||||
if (vaAssigned !== undefined) {
|
||||
|
|
@ -211,33 +222,33 @@ async function update_project_sf(req, res) {
|
|||
|
||||
} catch (error) {
|
||||
console.error("update_project_sf error:", error);
|
||||
|
||||
|
||||
// MongoDB duplicate key error
|
||||
if (error.code === 11000) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project with this name already exists"
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Project with this name already exists"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// MongoDB validation error
|
||||
if (error.name === 'ValidationError') {
|
||||
const errors = Object.values(error.errors).map(err => err.message);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Validation error",
|
||||
errors: errors
|
||||
errors: errors
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error",
|
||||
error: error.message
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
export {
|
||||
update_project_sf
|
||||
};
|
||||
|
|
@ -1,101 +1,102 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
|
||||
import authFetch from "../../../utils/authFetch.js";
|
||||
|
||||
// function to close open work tracks and create logout
|
||||
async function closePreviousSession(vaId, lastLog) {
|
||||
try {
|
||||
const currentTime = new Date();
|
||||
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
|
||||
const openWorkTracks = await WorkTrackModel.find({
|
||||
vaId,
|
||||
logId: lastLog._id,
|
||||
$or: [
|
||||
{ to: { $exists: false } },
|
||||
{ totalTime: 0 },
|
||||
{ to: null }
|
||||
]
|
||||
});
|
||||
// async function closePreviousSession(vaId, lastLog) {
|
||||
// try {
|
||||
// const currentTime = new Date();
|
||||
// // Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
|
||||
// const openWorkTracks = await WorkTrackModel.find({
|
||||
// vaId,
|
||||
// logId: lastLog._id,
|
||||
// $or: [
|
||||
// { to: { $exists: false } },
|
||||
// { totalTime: 0 },
|
||||
// { to: null }
|
||||
// ]
|
||||
// });
|
||||
|
||||
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
|
||||
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
|
||||
|
||||
// Close all open work tracks
|
||||
for (const workTrack of openWorkTracks) {
|
||||
const fromTime = new Date(workTrack.from);
|
||||
const totalTimeMs = currentTime - fromTime;
|
||||
// // Close all open work tracks
|
||||
// for (const workTrack of openWorkTracks) {
|
||||
// const fromTime = new Date(workTrack.from);
|
||||
// const totalTimeMs = currentTime - fromTime;
|
||||
|
||||
// Update the work track with end time and total time
|
||||
workTrack.to = currentTime;
|
||||
workTrack.totalTime = totalTimeMs;
|
||||
await workTrack.save();
|
||||
// // Update the work track with end time and total time
|
||||
// workTrack.to = currentTime;
|
||||
// workTrack.totalTime = totalTimeMs;
|
||||
// await workTrack.save();
|
||||
|
||||
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
|
||||
}
|
||||
// // console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
|
||||
// }
|
||||
|
||||
// Create a logout log entry
|
||||
const logoutLog = await LoggedModel.create({
|
||||
loggedType: "LOGOUT",
|
||||
vaId,
|
||||
vaEmail: lastLog.vaEmail, // Use email from previous log
|
||||
projectId: lastLog.projectId,
|
||||
time: currentTime // Use same timestamp for consistent logging
|
||||
});
|
||||
// // Create a logout log entry
|
||||
// const logoutLog = await LoggedModel.create({
|
||||
// loggedType: "LOGOUT",
|
||||
// vaId,
|
||||
// vaEmail: lastLog.vaEmail, // Use email from previous log
|
||||
// projectId: lastLog.projectId,
|
||||
// time: currentTime // Use same timestamp for consistent logging
|
||||
// });
|
||||
|
||||
// console.log(`Created logout log: ${logoutLog._id}`);
|
||||
// try {
|
||||
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||
// const sfDataSync = {
|
||||
// "_id": logoutLog._id,
|
||||
// "loggedType": "LOGOUT",
|
||||
// "vaId": vaId,
|
||||
// "vaEmail": lastLog.vaEmail,
|
||||
// "projectId": lastLog.projectId,
|
||||
// "time": logoutLog.time,
|
||||
// }
|
||||
// // console.log(`Created logout log: ${logoutLog._id}`);
|
||||
// try {
|
||||
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||
// const sfDataSync = {
|
||||
// "_id": logoutLog._id,
|
||||
// "loggedType": "LOGOUT",
|
||||
// "vaId": vaId,
|
||||
// "vaEmail": lastLog.vaEmail,
|
||||
// "projectId": lastLog.projectId,
|
||||
// "time": logoutLog.time,
|
||||
// }
|
||||
|
||||
// const response = await fetch(salesforceUrl, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// // Add any required headers for Salesforce authentication
|
||||
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||
// },
|
||||
// body: JSON.stringify(sfDataSync)
|
||||
// });
|
||||
// const response = await authFetch(salesforceUrl, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// // Add any required headers for Salesforce authentication
|
||||
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||
// },
|
||||
// body: JSON.stringify(sfDataSync)
|
||||
// });
|
||||
|
||||
// if (!response.ok) {
|
||||
// const errorText = await response.text();
|
||||
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
// }
|
||||
// if (!response.ok) {
|
||||
// const errorText = await response.text();
|
||||
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
// }
|
||||
|
||||
// const result = await response.json();
|
||||
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
// const result = await response.json();
|
||||
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
// return {
|
||||
// closedWorkTracks: openWorkTracks.length,
|
||||
// logoutLogId: logoutLog._id,
|
||||
// SyncSalesforce: result
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error("Failed to sync with Salesforce:", error.message);
|
||||
// console.log(error)
|
||||
// return {
|
||||
// closedWorkTracks: openWorkTracks.length,
|
||||
// logoutLogId: logoutLog._id,
|
||||
// SyncSalesforce: result
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error("Failed to sync with Salesforce:", error.message);
|
||||
// console.log(error)
|
||||
|
||||
// // Create a custom error to distinguish Salesforce errors
|
||||
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||
// salesforceError.isSalesforceError = true;
|
||||
// throw salesforceError;
|
||||
// }
|
||||
// // Create a custom error to distinguish Salesforce errors
|
||||
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||
// salesforceError.isSalesforceError = true;
|
||||
// throw salesforceError;
|
||||
// }
|
||||
|
||||
return {
|
||||
closedWorkTracks: openWorkTracks.length,
|
||||
logoutLogId: logoutLog._id,
|
||||
};
|
||||
// // return {
|
||||
// // closedWorkTracks: openWorkTracks.length,
|
||||
// // logoutLogId: logoutLog._id,
|
||||
// // };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error closing previous session:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// } catch (error) {
|
||||
// console.error("Error closing previous session:", error);
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Login VA
|
||||
async function va_login_callback(req, res) {
|
||||
|
|
@ -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
|
||||
|
||||
// Check if VA has any active sessions (LOGIN without LOGOUT)
|
||||
const activeSessions = await LoggedModel.find({
|
||||
vaId: user._id
|
||||
}).sort({ time: -1 });
|
||||
// const activeSessions = await LoggedModel.find({
|
||||
// vaId: user._id
|
||||
// }).sort({ time: -1 });
|
||||
|
||||
let closeResult = [];
|
||||
// let closeResult = [];
|
||||
|
||||
if (activeSessions.length > 0) {
|
||||
// Get the latest login to check if there's no corresponding logout
|
||||
const latestLog = activeSessions[0];
|
||||
closeResult = await closePreviousSession(user._id, latestLog);
|
||||
}
|
||||
// if (activeSessions.length > 0) {
|
||||
// // Get the latest login to check if there's no corresponding logout
|
||||
// const latestLog = activeSessions[0];
|
||||
// closeResult = await closePreviousSession(user._id, latestLog);
|
||||
// }
|
||||
|
||||
// Generate JWT accessToken with 24h expiry
|
||||
const accessToken = jwt.sign(
|
||||
|
|
@ -171,23 +172,23 @@ async function va_login_callback(req, res) {
|
|||
|
||||
let responseData = [];
|
||||
|
||||
if (closeResult) {
|
||||
responseData = {
|
||||
success: true,
|
||||
message: "Login Successfull",
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: {
|
||||
id: user._id,
|
||||
name: user.userDetails.firstName,
|
||||
email: user.userDetails.email,
|
||||
},
|
||||
previousSession: {
|
||||
closedWorkTracks: closeResult.closedWorkTracks,
|
||||
logoutLogId: closeResult.logoutLogId
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if (closeResult) {
|
||||
// responseData = {
|
||||
// success: true,
|
||||
// message: "Login Successfull",
|
||||
// accessToken,
|
||||
// refreshToken,
|
||||
// user: {
|
||||
// id: user._id,
|
||||
// name: user.userDetails.firstName,
|
||||
// email: user.userDetails.email,
|
||||
// },
|
||||
// previousSession: {
|
||||
// closedWorkTracks: closeResult.closedWorkTracks,
|
||||
// logoutLogId: closeResult.logoutLogId
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
responseData = {
|
||||
success: true,
|
||||
message: "Login Successfull",
|
||||
|
|
@ -199,7 +200,7 @@ async function va_login_callback(req, res) {
|
|||
email: user.userDetails.email,
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
// Return accessToken and user info
|
||||
return res.status(200).json(responseData);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
|
||||
import authFetch from "../../../utils/authFetch.js";
|
||||
|
||||
// Apply for leave
|
||||
async function va_leave_apply_function(req, res) {
|
||||
|
|
@ -158,13 +159,13 @@ async function va_leave_apply_function(req, res) {
|
|||
"dateTo": endDate.toISOString().split('T')[0],
|
||||
"totalDays": totalDays,
|
||||
"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
|
||||
"status": "PENDING",
|
||||
"rejectionReason": ""
|
||||
}
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -176,6 +177,7 @@ async function va_leave_apply_function(req, res) {
|
|||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
await LeavesModel.findByIdAndDelete(leaveRequest._id)
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../mod
|
|||
function getDayRange(date = new Date()) {
|
||||
const start = new Date(date);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
const end = new Date(date);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ function formatHoursToReadable(hours) {
|
|||
const wholeHours = Math.floor(hours);
|
||||
const decimalMinutes = (hours - wholeHours) * 60;
|
||||
const wholeMinutes = Math.round(decimalMinutes);
|
||||
|
||||
|
||||
if (wholeHours > 0 && wholeMinutes > 0) {
|
||||
return `${wholeHours}h ${wholeMinutes}m`;
|
||||
} else if (wholeHours > 0) {
|
||||
|
|
@ -26,246 +26,274 @@ function formatHoursToReadable(hours) {
|
|||
}
|
||||
}
|
||||
|
||||
// Helper function to check daily hours and create logout log if needed
|
||||
async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
|
||||
try {
|
||||
const today = new Date();
|
||||
const { start, end } = getDayRange(today);
|
||||
|
||||
// Get today's work entries for this VA and project
|
||||
// Exclude the current session if it's already in the database
|
||||
const query = {
|
||||
vaId,
|
||||
projectId,
|
||||
from: { $gte: start, $lte: end }
|
||||
};
|
||||
|
||||
if (currentSessionId) {
|
||||
query._id = { $ne: currentSessionId };
|
||||
}
|
||||
|
||||
const todayWorkEntries = await WorkTrackModel.find(query);
|
||||
|
||||
// Calculate total hours worked today
|
||||
let totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
||||
return total + (entry.totalTime || 0);
|
||||
}, 0);
|
||||
|
||||
// Add the additional time from current session if provided
|
||||
totalTodayMs += additionalTimeMs;
|
||||
|
||||
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
||||
|
||||
// Check if daily dedicated hours are completed
|
||||
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
|
||||
// Create automatic logout log
|
||||
const logoutLog = await LoggedModel.create({
|
||||
loggedType: "LOGOUT",
|
||||
vaId,
|
||||
vaEmail,
|
||||
projectId
|
||||
});
|
||||
// // Helper function to check daily hours and create logout log if needed
|
||||
// async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
|
||||
// try {
|
||||
// const today = new Date();
|
||||
// const { start, end } = getDayRange(today);
|
||||
|
||||
try {
|
||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||
const sfDataSync = {
|
||||
"_id": logoutLog._id,
|
||||
"loggedType": "LOGOUT",
|
||||
"vaId": vaId,
|
||||
"vaEmail": vaEmail,
|
||||
"projectId": projectId,
|
||||
"time": logoutLog.time,
|
||||
}
|
||||
// // Get today's work entries for this VA and project
|
||||
// // Exclude the current session if it's already in the database
|
||||
// const query = {
|
||||
// vaId,
|
||||
// projectId,
|
||||
// from: { $gte: start, $lte: end }
|
||||
// };
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// Add any required headers for Salesforce authentication
|
||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||
},
|
||||
body: JSON.stringify(sfDataSync)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
// if (currentSessionId) {
|
||||
// query._id = { $ne: currentSessionId };
|
||||
// }
|
||||
|
||||
const result = await response.json();
|
||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
return {
|
||||
shouldLogout: true,
|
||||
totalTodayHours: totalTodayHours,
|
||||
dedicatedHours: project.dedicatedHours,
|
||||
logoutLogId: logoutLog._id,
|
||||
SyncSalesforce: result
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to sync with Salesforce:", error.message);
|
||||
console.log(error)
|
||||
|
||||
// Create a custom error to distinguish Salesforce errors
|
||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||
salesforceError.isSalesforceError = true;
|
||||
throw salesforceError;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shouldLogout: false,
|
||||
totalTodayHours: totalTodayHours,
|
||||
dedicatedHours: project.dedicatedHours
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in checkDailyHoursAndLogout:", error);
|
||||
return { shouldLogout: false, error: error.message };
|
||||
}
|
||||
}
|
||||
// const todayWorkEntries = await WorkTrackModel.find(query);
|
||||
|
||||
// // Calculate total hours worked today
|
||||
// let totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
||||
// return total + (entry.totalTime || 0);
|
||||
// }, 0);
|
||||
|
||||
// // Add the additional time from current session if provided
|
||||
// totalTodayMs += additionalTimeMs;
|
||||
|
||||
// const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
||||
|
||||
// // Check if daily dedicated hours are completed
|
||||
// if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
|
||||
// // Create automatic logout log
|
||||
// const logoutLog = await LoggedModel.create({
|
||||
// loggedType: "LOGOUT",
|
||||
// vaId,
|
||||
// vaEmail,
|
||||
// projectId
|
||||
// });
|
||||
|
||||
// try {
|
||||
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||
// const sfDataSync = {
|
||||
// "_id": logoutLog._id,
|
||||
// "loggedType": "LOGOUT",
|
||||
// "vaId": vaId,
|
||||
// "vaEmail": vaEmail,
|
||||
// "projectId": projectId,
|
||||
// "time": logoutLog.time,
|
||||
// }
|
||||
|
||||
// const response = await fetch(salesforceUrl, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// // Add any required headers for Salesforce authentication
|
||||
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||
// },
|
||||
// body: JSON.stringify(sfDataSync)
|
||||
// });
|
||||
|
||||
// if (!response.ok) {
|
||||
// const errorText = await response.text();
|
||||
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
// }
|
||||
|
||||
// const result = await response.json();
|
||||
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
// return {
|
||||
// shouldLogout: true,
|
||||
// totalTodayHours: totalTodayHours,
|
||||
// dedicatedHours: project.dedicatedHours,
|
||||
// logoutLogId: logoutLog._id,
|
||||
// SyncSalesforce: result
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error("Failed to sync with Salesforce:", error.message);
|
||||
// console.log(error)
|
||||
|
||||
// // Create a custom error to distinguish Salesforce errors
|
||||
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||
// salesforceError.isSalesforceError = true;
|
||||
// throw salesforceError;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// shouldLogout: false,
|
||||
// totalTodayHours: totalTodayHours,
|
||||
// dedicatedHours: project.dedicatedHours
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error("Error in checkDailyHoursAndLogout:", error);
|
||||
// return { shouldLogout: false, error: error.message };
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// Getting logged hours in a specific project of VA
|
||||
async function va_log_track_work_callback(req, res) {
|
||||
try {
|
||||
const accessToken = req.headers.token;
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
}
|
||||
// const accessToken = req.headers.token;
|
||||
// if (!accessToken) {
|
||||
// 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) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
// if (!decoded || !decoded.email) {
|
||||
// return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
// }
|
||||
|
||||
const vaEmail = decoded.email;
|
||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
// const vaEmail = decoded.email;
|
||||
// const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
// if (!vaUser) {
|
||||
// return res.status(404).json({ success: false, message: "VA not found" });
|
||||
// }
|
||||
|
||||
const vaId = vaUser._id;
|
||||
// const vaId = vaUser._id;
|
||||
const payload = req.body || {};
|
||||
const { projectId, logId, title, appName, url = null } = payload;
|
||||
|
||||
const project = await ProjectModel.findById(projectId);
|
||||
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);
|
||||
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
|
||||
|
||||
const logEntry = await LoggedModel.findById(logId);
|
||||
if (!logEntry) return res.status(400).json({ success: false, message: "Log entry not found" });
|
||||
|
||||
// Check last log entry for this VA and project
|
||||
const lastLog = await LoggedModel.findOne({ vaId, projectId })
|
||||
.sort({ time: -1 }) // get the most recent log
|
||||
.lean();
|
||||
|
||||
// If the last log is a LOGOUT, prevent new work tracking
|
||||
if (lastLog.loggedType === "LOGOUT") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "User has already logged out from this project",
|
||||
});
|
||||
if (logs.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "No logs to track" });
|
||||
}
|
||||
|
||||
if (!projectId || !logId || !title || !appName) {
|
||||
return res.status(400).json({ success: false, message: "Missing Props" });
|
||||
}
|
||||
logs[0].from = logEntry.time; // Ensure the first log's 'from' matches the main log time
|
||||
|
||||
const firstFromLogTime = new Date(logs[0].from);
|
||||
const firstToLogTime = new Date(logs[0].to);
|
||||
const firstTotalLogTime = Math.abs(firstToLogTime - firstFromLogTime);
|
||||
logs[0].totalTime = firstTotalLogTime;
|
||||
|
||||
|
||||
// // Check last log entry for this VA and project
|
||||
// const lastLog = await LoggedModel.findOne({ vaId, projectId })
|
||||
// .sort({ time: -1 }) // get the most recent log
|
||||
// .lean();
|
||||
|
||||
// // If the last log is a LOGOUT, prevent new work tracking
|
||||
// if (lastLog.loggedType === "LOGOUT") {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: "User has already logged out from this project",
|
||||
// });
|
||||
// }
|
||||
|
||||
// Handle POST → create entry
|
||||
if (req.method === "POST") {
|
||||
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
|
||||
|
||||
if (dailyCheck.shouldLogout) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Cannot start work - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`,
|
||||
autoLogout: true
|
||||
});
|
||||
}
|
||||
// if (req.method === "POST") {
|
||||
// const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
|
||||
// const createdTracks = await Promise.all(
|
||||
// logs.map(async (log) => {
|
||||
// // Calculate total time from timestamps
|
||||
// const fromTime = new Date(log.from);
|
||||
// const toTime = new Date(log.to);
|
||||
// const totalTime = Math.abs(toTime - fromTime);
|
||||
// const createdTrack = await WorkTrackModel.create({
|
||||
// vaId,
|
||||
// projectId,
|
||||
// logId,
|
||||
// title: log.title,
|
||||
// appName: log.appName,
|
||||
// url: log.url,
|
||||
// from: log.from,
|
||||
// to: log.to,
|
||||
// totalTime: totalTime,
|
||||
// });
|
||||
|
||||
// return createdTrack;
|
||||
// })
|
||||
// );
|
||||
|
||||
const fromLogTime = new Date(logs[0].from);
|
||||
const toLogTime = new Date(logs[logs.length -1].to);
|
||||
const totalLogTime = Math.abs(toLogTime - fromLogTime);
|
||||
|
||||
const createdTrack = await WorkTrackModel.create({
|
||||
vaId,
|
||||
projectId,
|
||||
logId,
|
||||
title,
|
||||
appName,
|
||||
url,
|
||||
from: Date.now(),
|
||||
from: logs[0].from,
|
||||
to: logs[logs.length -1].to,
|
||||
totalTime: totalLogTime,
|
||||
logs: logs
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: "Work tracking started",
|
||||
data: createdTrack,
|
||||
dailyHours: {
|
||||
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
|
||||
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
|
||||
remaining: formatHoursToReadable(dailyCheck.dedicatedHours - dailyCheck.totalTodayHours)
|
||||
}
|
||||
message: "Work tracking created",
|
||||
data: createdTrack
|
||||
});
|
||||
}
|
||||
// }
|
||||
|
||||
// Handle PATCH → update entry (add 'to' and calculate total time)
|
||||
if (req.method === "PATCH") {
|
||||
const { id } = payload; // expecting the workTrack record ID to update
|
||||
if (!id) {
|
||||
return res.status(400).json({ success: false, message: "Missing workTrack ID" });
|
||||
}
|
||||
// // Handle PATCH → update entry (add 'to' and calculate total time)
|
||||
// if (req.method === "PATCH") {
|
||||
// const { id } = payload; // expecting the workTrack record ID to update
|
||||
// if (!id) {
|
||||
// return res.status(400).json({ success: false, message: "Missing workTrack ID" });
|
||||
// }
|
||||
|
||||
const workEntry = await WorkTrackModel.findById(id);
|
||||
if (!workEntry) {
|
||||
return res.status(404).json({ success: false, message: "Work entry not found" });
|
||||
}
|
||||
// const workEntry = await WorkTrackModel.findById(id);
|
||||
// if (!workEntry) {
|
||||
// return res.status(404).json({ success: false, message: "Work entry not found" });
|
||||
// }
|
||||
|
||||
const toTime = new Date();
|
||||
const fromTime = new Date(workEntry.from);
|
||||
const totalTimeMs = toTime - fromTime;
|
||||
// const toTime = new Date();
|
||||
// const fromTime = new Date(workEntry.from);
|
||||
// const totalTimeMs = toTime - fromTime;
|
||||
|
||||
// Optional: store totalTime in minutes or hours instead of raw ms
|
||||
const hours = Math.floor(totalTimeMs / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000);
|
||||
// // Optional: store totalTime in minutes or hours instead of raw ms
|
||||
// const hours = Math.floor(totalTimeMs / (1000 * 60 * 60));
|
||||
// const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
// const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000);
|
||||
|
||||
const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`;
|
||||
// const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`;
|
||||
|
||||
// Check if daily dedicated hours are completed after this work session
|
||||
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
|
||||
// // Check if daily dedicated hours are completed after this work session
|
||||
// const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
|
||||
|
||||
workEntry.to = toTime;
|
||||
workEntry.totalTime = totalTimeMs; // store as number of minutes
|
||||
await workEntry.save();
|
||||
// workEntry.to = toTime;
|
||||
// workEntry.totalTime = totalTimeMs; // store as number of minutes
|
||||
// await workEntry.save();
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
message: "Work tracking stopped",
|
||||
data: {
|
||||
id: workEntry._id,
|
||||
from: workEntry.from,
|
||||
to: workEntry.to,
|
||||
formattedTotalTime,
|
||||
totalTimeMs,
|
||||
sessionHours: totalTimeMs / (1000 * 60 * 60)
|
||||
},
|
||||
dailyHours: {
|
||||
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
|
||||
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
|
||||
remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
|
||||
}
|
||||
};
|
||||
// const response = {
|
||||
// success: true,
|
||||
// message: "Work tracking stopped",
|
||||
// data: {
|
||||
// id: workEntry._id,
|
||||
// from: workEntry.from,
|
||||
// to: workEntry.to,
|
||||
// formattedTotalTime,
|
||||
// totalTimeMs,
|
||||
// sessionHours: totalTimeMs / (1000 * 60 * 60)
|
||||
// },
|
||||
// dailyHours: {
|
||||
// todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
|
||||
// dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
|
||||
// remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
|
||||
// }
|
||||
// };
|
||||
|
||||
// If auto-logout was triggered, add that info to response
|
||||
if (dailyCheck.shouldLogout) {
|
||||
response.autoLogout = true;
|
||||
response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
|
||||
response.logoutLogId = dailyCheck.logoutLogId;
|
||||
}
|
||||
// // If auto-logout was triggered, add that info to response
|
||||
// if (dailyCheck.shouldLogout) {
|
||||
// response.autoLogout = true;
|
||||
// response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
|
||||
// response.logoutLogId = dailyCheck.logoutLogId;
|
||||
// }
|
||||
|
||||
return res.status(200).json(response);
|
||||
}
|
||||
// return res.status(200).json(response);
|
||||
// }
|
||||
|
||||
// Fallback for unsupported methods
|
||||
return res.status(405).json({ success: false, message: "Method not allowed" });
|
||||
// return res.status(405).json({ success: false, message: "Method not allowed" });
|
||||
} catch (error) {
|
||||
return res.status(500).send("Internal Server Error");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { VAModel } from "../../../models/vaModel.js";
|
||||
import authFetch from "../../../utils/authFetch.js";
|
||||
|
||||
async function va_payslip_data_function(req, res) {
|
||||
try {
|
||||
|
|
@ -9,9 +10,9 @@ async function va_payslip_data_function(req, res) {
|
|||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import authFetch from "../../../utils/authFetch.js";
|
||||
|
||||
async function va_payroll_schedule_function(req, res) {
|
||||
try {
|
||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/payroll-schedule`;
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ async function va_remove_function(req, res) {
|
|||
if (projectIds.length > 0) {
|
||||
const projectResult = await ProjectModel.updateMany(
|
||||
{ _id: { $in: projectIds } },
|
||||
{ $pull: { vaAssigned: vaId } }
|
||||
{ $pull: { "vaAssigned.vaId": vaId } }
|
||||
);
|
||||
projectsUpdated = projectResult.modifiedCount;
|
||||
console.log(`Removed VA from ${projectsUpdated} projects`);
|
||||
|
|
|
|||
|
|
@ -21,16 +21,12 @@ async function va_profile_callback(req, res) {
|
|||
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 })
|
||||
.select("-userDetails.password") // hide password
|
||||
.populate("clientIds", "userDetails.email userDetails.name") // optional populate
|
||||
.populate("projectIds", "name totalLoggedHours") // optional populate
|
||||
.lean();
|
||||
|
||||
console.log("Line 33" , vaUser)
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { VAModel } from "../../../models/vaModel.js";
|
|||
// Register a new VA
|
||||
async function va_profile_update_callback(req, res) {
|
||||
try {
|
||||
const sf_token = req.headers.sf_token;
|
||||
const sf_token = req.headers["x-auth-token"];
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
let vaUser;
|
||||
|
|
@ -68,7 +68,7 @@ async function va_profile_update_callback(req, res) {
|
|||
const allowedFields = [
|
||||
"firstName", "lastName", "dob", "age", "address",
|
||||
"city", "state", "country", "phone", "skype",
|
||||
"paymentMethod", "payoneerAccount", "veemAccount"
|
||||
"paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail"
|
||||
];
|
||||
|
||||
let hasUpdates = false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import { VAModel } from "../../../models/vaModel.js";
|
||||
import authFetch from "../../../utils/authFetch.js";
|
||||
|
||||
// Register a new VA
|
||||
async function va_register(req, res) {
|
||||
|
|
@ -53,7 +54,7 @@ async function va_register(req, res) {
|
|||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
|
||||
delete created.userDetails._id;
|
||||
created.userDetails._id = created._id; // Include VA's MongoDB ID
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -74,7 +75,7 @@ async function va_register(req, res) {
|
|||
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to sync with Salesforce:", error.message);
|
||||
|
||||
await VAModel.findOneAndDelete({_id: created.userDetails._id});
|
||||
// Create a custom error to distinguish Salesforce errors
|
||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||
salesforceError.isSalesforceError = true;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import mongoose from "mongoose";
|
||||
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
|
||||
import authFetch from "../../../utils/authFetch.js";
|
||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
||||
|
||||
// Helper function to get start and end of day in UTC
|
||||
function getDayRange(date = new Date()) {
|
||||
const start = new Date(date);
|
||||
start.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
const end = new Date(date);
|
||||
end.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
|
|
@ -15,7 +18,7 @@ function formatHoursToReadable(hours) {
|
|||
const wholeHours = Math.floor(hours);
|
||||
const decimalMinutes = (hours - wholeHours) * 60;
|
||||
const wholeMinutes = Math.round(decimalMinutes);
|
||||
|
||||
|
||||
if (wholeHours > 0 && wholeMinutes > 0) {
|
||||
return `${wholeHours}h ${wholeMinutes}m`;
|
||||
} else if (wholeHours > 0) {
|
||||
|
|
@ -28,11 +31,11 @@ function formatHoursToReadable(hours) {
|
|||
// Helper function to parse ISO time string
|
||||
function parseISOTimeString(timeString) {
|
||||
if (!timeString) return new Date();
|
||||
|
||||
|
||||
try {
|
||||
// Parse ISO string directly
|
||||
const parsedDate = new Date(timeString);
|
||||
|
||||
|
||||
// Check if the date is valid
|
||||
if (isNaN(parsedDate.getTime())) {
|
||||
throw new Error('Invalid date');
|
||||
|
|
@ -64,84 +67,84 @@ function getCurrentUTCTime() {
|
|||
|
||||
// function parseTimeToMinutes(timeString) {
|
||||
// if (!timeString) return null;
|
||||
|
||||
|
||||
// const [hours, minutes] = timeString.split(':').map(Number);
|
||||
// return hours * 60 + (minutes || 0);
|
||||
// }
|
||||
|
||||
// function parseDelayToMinutes(delayString) {
|
||||
// if (!delayString) return 0;
|
||||
|
||||
|
||||
// if (delayString.includes('h')) {
|
||||
// const hours = parseInt(delayString.replace('h', '').trim());
|
||||
// return hours * 60;
|
||||
// } else if (delayString.includes('m')) {
|
||||
// return parseInt(delayString.replace('m', '').trim());
|
||||
// }
|
||||
|
||||
|
||||
// return parseInt(delayString) || 0; // Default to minutes if no unit specified
|
||||
// }
|
||||
|
||||
// function checkScheduleCompliance(currentTime, project, vaStatus, type) {
|
||||
// const warnings = [];
|
||||
|
||||
|
||||
// // If project doesn't have schedule or VA is PARTTIME with flexible allowed, no schedule restrictions
|
||||
// if (!project.vaWorkingScheduleStart || !project.vaWorkingScheduleEnd) {
|
||||
// return { isCompliant: true, warnings };
|
||||
// }
|
||||
|
||||
|
||||
// const currentDate = new Date(currentTime);
|
||||
// const currentMinutes = currentDate.getUTCHours() * 60 + currentDate.getUTCMinutes();
|
||||
|
||||
|
||||
// const scheduleStartMinutes = parseTimeToMinutes(project.vaWorkingScheduleStart);
|
||||
// const scheduleEndMinutes = parseTimeToMinutes(project.vaWorkingScheduleEnd);
|
||||
// const allowedStartDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedStart);
|
||||
// const allowedEndDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedEnd);
|
||||
|
||||
|
||||
// // Calculate allowed time ranges
|
||||
// const allowedStartMinutes = scheduleStartMinutes - allowedStartDelay;
|
||||
// const allowedEndMinutes = scheduleEndMinutes + allowedEndDelay;
|
||||
|
||||
|
||||
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
|
||||
|
||||
|
||||
// // Check for PARTTIME with flexible schedule
|
||||
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) {
|
||||
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowed) {
|
||||
// return { isCompliant: true, warnings };
|
||||
// }
|
||||
|
||||
|
||||
// // For all other cases, check schedule compliance
|
||||
// if (type === "LOGIN") {
|
||||
// // Check if login is too early
|
||||
// if (currentMinutes < allowedStartMinutes) {
|
||||
// warnings.push("early_login");
|
||||
// }
|
||||
|
||||
|
||||
// // Check if login is after shift end
|
||||
// if (currentMinutes > scheduleEndMinutes) {
|
||||
// warnings.push("working_after_shift_end");
|
||||
// }
|
||||
|
||||
|
||||
// // Check if login is within allowed range
|
||||
// // const isWithinAllowedRange = currentMinutes >= allowedStartMinutes && currentMinutes <= allowedEndMinutes;
|
||||
|
||||
|
||||
// return {
|
||||
// isCompliant: true, // Allow login regardless of time
|
||||
// warnings
|
||||
// };
|
||||
|
||||
|
||||
// } else if (type === "LOGOUT") {
|
||||
// // Check if logout is too late
|
||||
// if (currentMinutes > allowedEndMinutes) {
|
||||
// warnings.push("late_logout");
|
||||
// }
|
||||
|
||||
|
||||
// // For logout, we're generally more permissive - allow logout even if late
|
||||
// return {
|
||||
// isCompliant: true, // Allow logout regardless of time
|
||||
// warnings
|
||||
// };
|
||||
// }
|
||||
|
||||
|
||||
// return { isCompliant: true, warnings };
|
||||
// }
|
||||
|
||||
|
|
@ -149,7 +152,7 @@ function getCurrentUTCTime() {
|
|||
async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
||||
try {
|
||||
const currentTime = logTime;
|
||||
|
||||
|
||||
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
|
||||
const openWorkTracks = await WorkTrackModel.find({
|
||||
vaId,
|
||||
|
|
@ -160,22 +163,22 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
|||
{ to: null }
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
|
||||
|
||||
|
||||
// Close all open work tracks
|
||||
for (const workTrack of openWorkTracks) {
|
||||
const fromTime = new Date(workTrack.from);
|
||||
const totalTimeMs = currentTime - fromTime;
|
||||
|
||||
|
||||
// Update the work track with end time and total time
|
||||
workTrack.to = currentTime;
|
||||
workTrack.totalTime = totalTimeMs;
|
||||
await workTrack.save();
|
||||
|
||||
|
||||
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
|
||||
}
|
||||
|
||||
|
||||
// Create a logout log entry
|
||||
const logoutLog = await LoggedModel.create({
|
||||
loggedType: "LOGOUT",
|
||||
|
|
@ -184,20 +187,20 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
|||
projectId: lastLog.projectId,
|
||||
time: currentTime // Use same timestamp for consistent logging
|
||||
});
|
||||
|
||||
|
||||
// console.log(`Created logout log: ${logoutLog._id}`);
|
||||
try {
|
||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||
const sfDataSync = {
|
||||
"_id": logoutLog._id,
|
||||
"loggedType": "LOGOUT",
|
||||
"vaId": vaId,
|
||||
"vaEmail": lastLog.vaEmail,
|
||||
"projectId": lastLog.projectId,
|
||||
"time": logoutLog.time,
|
||||
}
|
||||
"_id": logoutLog._id,
|
||||
"loggedType": "LOGOUT",
|
||||
"vaId": vaId,
|
||||
"vaEmail": lastLog.vaEmail,
|
||||
"projectId": lastLog.projectId,
|
||||
"time": logoutLog.time,
|
||||
}
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -206,7 +209,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
|||
},
|
||||
body: JSON.stringify(sfDataSync)
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
|
|
@ -214,7 +217,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
|||
|
||||
const result = await response.json();
|
||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
|
||||
return {
|
||||
closedWorkTracks: openWorkTracks.length,
|
||||
logoutLogId: logoutLog._id,
|
||||
|
|
@ -223,13 +226,13 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
|
|||
} catch (error) {
|
||||
console.error("Failed to sync with Salesforce:", error.message);
|
||||
console.log(error)
|
||||
|
||||
|
||||
// Create a custom error to distinguish Salesforce errors
|
||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||
salesforceError.isSalesforceError = true;
|
||||
throw salesforceError;
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error closing previous session:", error);
|
||||
throw error;
|
||||
|
|
@ -242,7 +245,7 @@ async function va_project_log_callback(req, res) {
|
|||
const payload = req.body || {};
|
||||
const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload;
|
||||
// console.log("Received payload:", payload);
|
||||
|
||||
|
||||
if (!vaId || !vaEmail || !projectId || !type) {
|
||||
return res.status(400).send("Missing required properties");
|
||||
}
|
||||
|
|
@ -250,11 +253,11 @@ async function va_project_log_callback(req, res) {
|
|||
// Determine the log time based on received time (both in UTC)
|
||||
const currentTime = getCurrentUTCTime(); // Current UTC time
|
||||
let logTime = currentTime;
|
||||
|
||||
|
||||
if (receivedTimeString) {
|
||||
// Parse the received ISO time string
|
||||
const receivedTime = parseISOTimeString(receivedTimeString);
|
||||
|
||||
|
||||
// Check if the difference is less than 10 seconds
|
||||
if (isTimeWithinThreshold(receivedTime, currentTime, 10)) {
|
||||
logTime = receivedTime;
|
||||
|
|
@ -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)
|
||||
if (type === "LOGIN") {
|
||||
const { start, end } = getDayRange(); // UTC day range
|
||||
|
|
@ -298,18 +424,20 @@ async function va_project_log_callback(req, res) {
|
|||
projectId,
|
||||
from: { $gte: start, $lte: end }
|
||||
});
|
||||
|
||||
|
||||
const totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
||||
return total + (entry.totalTime || 0);
|
||||
}, 0);
|
||||
|
||||
|
||||
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
||||
|
||||
|
||||
const dayNumber = getDayNumber(logTime);
|
||||
|
||||
// Check if daily hours are already completed
|
||||
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
|
||||
if (project.dedicatedHours[dayNumber] > 0 && totalTodayHours >= project.dedicatedHours[dayNumber]) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`,
|
||||
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours[dayNumber])})`,
|
||||
// warnings: scheduleCheck.warnings
|
||||
});
|
||||
}
|
||||
|
|
@ -329,10 +457,10 @@ async function va_project_log_callback(req, res) {
|
|||
// console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`);
|
||||
|
||||
// Now proceed to create new login
|
||||
const createdLog = await LoggedModel.create({
|
||||
loggedType: "LOGIN",
|
||||
vaId,
|
||||
vaEmail,
|
||||
const createdLog = await LoggedModel.create({
|
||||
loggedType: "LOGIN",
|
||||
vaId,
|
||||
vaEmail,
|
||||
projectId,
|
||||
time: logTime
|
||||
});
|
||||
|
|
@ -340,15 +468,15 @@ async function va_project_log_callback(req, res) {
|
|||
try {
|
||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||
const sfData = {
|
||||
"_id": createdLog._id,
|
||||
"loggedType": "LOGIN",
|
||||
"vaId": vaId,
|
||||
"vaEmail": vaEmail,
|
||||
"projectId": projectId,
|
||||
"time": createdLog.time,
|
||||
}
|
||||
"_id": createdLog._id,
|
||||
"loggedType": "LOGIN",
|
||||
"vaId": vaId,
|
||||
"vaEmail": vaEmail,
|
||||
"projectId": projectId,
|
||||
"time": createdLog.time,
|
||||
}
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -357,7 +485,7 @@ async function va_project_log_callback(req, res) {
|
|||
},
|
||||
body: JSON.stringify(sfData)
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
|
|
@ -365,11 +493,11 @@ async function va_project_log_callback(req, res) {
|
|||
|
||||
const result = await response.json();
|
||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${type} Successfully`,
|
||||
logId: createdLog._id,
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: `${type} Successfully`,
|
||||
logId: createdLog._id,
|
||||
SyncSalesforce: result,
|
||||
previousSession: {
|
||||
closedWorkTracks: closeResult.closedWorkTracks,
|
||||
|
|
@ -382,7 +510,7 @@ async function va_project_log_callback(req, res) {
|
|||
} 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;
|
||||
|
|
@ -391,10 +519,10 @@ async function va_project_log_callback(req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
const createdLog = await LoggedModel.create({
|
||||
loggedType: type,
|
||||
vaId,
|
||||
vaEmail,
|
||||
const createdLog = await LoggedModel.create({
|
||||
loggedType: type,
|
||||
vaId,
|
||||
vaEmail,
|
||||
projectId,
|
||||
time: logTime
|
||||
});
|
||||
|
|
@ -402,15 +530,15 @@ async function va_project_log_callback(req, res) {
|
|||
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,
|
||||
}
|
||||
"_id": createdLog._id,
|
||||
"loggedType": type,
|
||||
"vaId": vaId,
|
||||
"vaEmail": vaEmail,
|
||||
"projectId": projectId,
|
||||
"time": createdLog.time,
|
||||
}
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -419,7 +547,7 @@ async function va_project_log_callback(req, res) {
|
|||
},
|
||||
body: JSON.stringify(sfData)
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
|
|
@ -427,11 +555,11 @@ async function va_project_log_callback(req, res) {
|
|||
|
||||
const result = await response.json();
|
||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${type} Successfully`,
|
||||
logId: createdLog._id,
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${type} Successfully`,
|
||||
logId: createdLog._id,
|
||||
SyncSalesforce: result,
|
||||
timeUsed: receivedTimeString ? 'received' : 'current',
|
||||
actualTime: createdLog.time,
|
||||
|
|
@ -440,7 +568,7 @@ async function va_project_log_callback(req, res) {
|
|||
} 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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
|
||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
||||
|
||||
function formatHMS(ms) {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
|
|
@ -72,7 +73,7 @@ async function va_projects_log_list_function(req, res) {
|
|||
|
||||
console.log("VA ID:", vaId.toString());
|
||||
|
||||
projectList = await ProjectModel.find({ vaAssigned: vaUser._id });
|
||||
projectList = await ProjectModel.find({ "vaAssigned.vaId": vaUser._id });
|
||||
}
|
||||
|
||||
if (!projectList.length) {
|
||||
|
|
@ -110,13 +111,15 @@ async function va_projects_log_list_function(req, res) {
|
|||
const lastLog = logs[logs.length - 1];
|
||||
const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY";
|
||||
|
||||
const dayNumber = getDayNumber();
|
||||
|
||||
finalReport.push({
|
||||
projectId: project._id,
|
||||
projectName: project.name,
|
||||
activityStatus,
|
||||
totalWorkTime: formatHMS(totalWorkMs),
|
||||
totalWorkMs,
|
||||
dedicatedHours: formatHoursToReadable(project?.dedicatedHours),
|
||||
dedicatedHours: formatHoursToReadable(project?.dedicatedHours[dayNumber]) || "0h",
|
||||
totalLogs: logs.length,
|
||||
status: project?.status
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function formatHMS(ms) {
|
|||
|
||||
async function va_projects_report_function(req, res) {
|
||||
try {
|
||||
const sf_token = req.headers.sf_token;
|
||||
const sf_token = req.headers["x-auth-token"];
|
||||
let vaId;
|
||||
let vaUser;
|
||||
let vaEmail;
|
||||
|
|
@ -49,9 +49,38 @@ async function va_projects_report_function(req, res) {
|
|||
|
||||
// Default: current day range
|
||||
const now = new Date();
|
||||
const fromDate = from && from !== "undefined" ? new Date(from) : new Date(now.setHours(0, 0, 0, 0));
|
||||
const toDate = to && to !== "undefined" ? new Date(to) : new Date();
|
||||
toDate.setHours(23, 59, 59, 999);
|
||||
let fromDate, toDate;
|
||||
|
||||
if (from && from !== "undefined" && from !== "null") {
|
||||
fromDate = new Date(from);
|
||||
// If only date string provided (YYYY-MM-DD), set to start of day
|
||||
if (from.length === 10) {
|
||||
fromDate.setHours(0, 0, 0, 0);
|
||||
}
|
||||
} else {
|
||||
// Default: 15 days ago
|
||||
fromDate = new Date();
|
||||
fromDate.setHours(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
if (to && to !== "undefined" && to !== "null") {
|
||||
toDate = new Date(to);
|
||||
// If only date string provided (YYYY-MM-DD), set to end of day
|
||||
if (to.length === 10) {
|
||||
toDate.setHours(23, 59, 59, 999);
|
||||
}
|
||||
} else {
|
||||
// Default: today end of day
|
||||
toDate = new Date();
|
||||
toDate.setHours(23, 59, 59, 999);
|
||||
}
|
||||
|
||||
if (fromDate > toDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid date range: 'from' date must be before 'to' date"
|
||||
});
|
||||
}
|
||||
|
||||
// Get all VA login/logout logs for this project in range
|
||||
const logs = await LoggedModel.find({
|
||||
|
|
@ -61,8 +90,35 @@ async function va_projects_report_function(req, res) {
|
|||
}).sort({ time: 1 }).lean();
|
||||
|
||||
if (!logs.length) {
|
||||
return res.json({ success: true, message: "No logs found for this project", 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
|
||||
const vaIds = [...new Set(logs.map(log => log.vaId?.toString()))].filter(Boolean);
|
||||
|
|
@ -73,55 +129,120 @@ async function va_projects_report_function(req, res) {
|
|||
|
||||
const projectReport = [];
|
||||
|
||||
for (const vaId of vaIds) {
|
||||
const vaLogList = logs.filter(l => l.vaId.toString() === vaId);
|
||||
for (const currentVaId of vaIds) {
|
||||
const vaLogList = logs.filter(l => l.vaId.toString() === currentVaId);
|
||||
let sessions = [];
|
||||
let currentLogin = null;
|
||||
const vaWorkTracks = workTracks.filter(w => w.vaId.toString() === currentVaId);
|
||||
|
||||
// Pair login → logout
|
||||
for (const entry of vaLogList) {
|
||||
if (entry.loggedType === "LOGIN") {
|
||||
currentLogin = entry;
|
||||
} else if (entry.loggedType === "LOGOUT" && currentLogin) {
|
||||
sessions.push({ from: currentLogin.time, to: entry.time });
|
||||
sessions.push({ from: currentLogin.time, to: entry.time, logId: currentLogin._id });
|
||||
currentLogin = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If still logged in, close at end of day
|
||||
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;
|
||||
const sessionDetails = [];
|
||||
|
||||
for (const s of sessions) {
|
||||
const workData = await WorkTrackModel.find({
|
||||
vaId,
|
||||
projectId,
|
||||
from: { $gte: s.from },
|
||||
to: { $lte: s.to }
|
||||
}).sort({ from: 1 }).lean();
|
||||
// for (const s of sessions) {
|
||||
// const workData = await WorkTrackModel.find({
|
||||
// vaId,
|
||||
// projectId,
|
||||
// from: { $gte: s.from },
|
||||
// to: { $lte: s.to }
|
||||
// }).sort({ from: 1 }).lean();
|
||||
|
||||
const tabs = workData.map(t => ({
|
||||
title: t.title,
|
||||
appName: t.appName,
|
||||
from: t.from,
|
||||
to: t.to,
|
||||
durationMs: t.totalTime,
|
||||
durationFormatted: formatHMS(t.totalTime),
|
||||
}));
|
||||
// const tabs = workData.map(t => ({
|
||||
// title: t.title,
|
||||
// appName: t.appName,
|
||||
// from: t.from,
|
||||
// to: t.to,
|
||||
// durationMs: t.totalTime,
|
||||
// durationFormatted: formatHMS(t.totalTime),
|
||||
// }));
|
||||
|
||||
const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0);
|
||||
totalWorkMs += sessionWorkMs;
|
||||
// const sessionWorkMs = workData.reduce((sum, w) => sum + (w.totalTime || 0), 0);
|
||||
// totalWorkMs += sessionWorkMs;
|
||||
|
||||
// sessionDetails.push({
|
||||
// from: s.from,
|
||||
// to: s.to,
|
||||
// totalSessionTime: formatHMS(sessionWorkMs),
|
||||
// tabs,
|
||||
// });
|
||||
// }
|
||||
|
||||
for (const session of sessions) {
|
||||
// Find work tracks for this specific session (matching logId)
|
||||
const sessionWorkTracks = vaWorkTracks.filter(
|
||||
track => track.logId && track.logId.toString() === session.logId.toString()
|
||||
);
|
||||
|
||||
let sessionTabs = [];
|
||||
let sessionTotalMs = 0;
|
||||
|
||||
// Extract all logs from work tracks for this session
|
||||
for (const workTrack of sessionWorkTracks) {
|
||||
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
||||
// Process each log entry in the logs array
|
||||
for (const logEntry of workTrack.logs) {
|
||||
// Calculate duration for this log entry
|
||||
const logFrom = new Date(logEntry.from);
|
||||
const logTo = new Date(logEntry.to || workTrack.to || session.to);
|
||||
const logDuration = logEntry.totalTime || (logTo - logFrom);
|
||||
|
||||
sessionTotalMs += logDuration;
|
||||
|
||||
sessionTabs.push({
|
||||
title: logEntry.title || "",
|
||||
appName: logEntry.appName || "",
|
||||
url: logEntry.url || null,
|
||||
from: logFrom,
|
||||
to: logTo,
|
||||
durationMs: logDuration,
|
||||
durationFormatted: formatHMS(logDuration),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also add the main work track if it has separate data
|
||||
if (workTrack.totalTime && workTrack.totalTime > 0) {
|
||||
sessionTotalMs += workTrack.totalTime;
|
||||
|
||||
// Only add if we don't have logs array data
|
||||
if (!workTrack.logs || workTrack.logs.length === 0) {
|
||||
sessionTabs.push({
|
||||
title: workTrack.title || "",
|
||||
appName: workTrack.appName || "",
|
||||
url: workTrack.url || null,
|
||||
from: workTrack.from,
|
||||
to: workTrack.to,
|
||||
durationMs: workTrack.totalTime,
|
||||
durationFormatted: formatHMS(workTrack.totalTime),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalWorkMs += sessionTotalMs;
|
||||
|
||||
sessionDetails.push({
|
||||
from: s.from,
|
||||
to: s.to,
|
||||
totalSessionTime: formatHMS(sessionWorkMs),
|
||||
tabs,
|
||||
from: session.from,
|
||||
to: session.to,
|
||||
totalSessionTime: formatHMS(sessionTotalMs),
|
||||
totalSessionMs: sessionTotalMs,
|
||||
workTracksCount: sessionWorkTracks.length,
|
||||
tabs: sessionTabs.sort((a, b) => a.from - b.from), // Sort tabs by time
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
import mongoose from "mongoose";
|
||||
import { AdjustedHoursModel } from "../../../models/vaModel.js";
|
||||
|
||||
// Get all adjustment requests (with filtering options)
|
||||
async function get_adjustment_requests_list(req, res) {
|
||||
try {
|
||||
const { status, projectId, vaId } = req.params;
|
||||
|
||||
const { status, projectId, vaId } = req.query;
|
||||
|
||||
// Build filter object
|
||||
const filter = {};
|
||||
if (status) filter.status = status;
|
||||
if (projectId) filter.projectId = projectId;
|
||||
if (vaId) filter.vaId = vaId;
|
||||
// if (status) filter.status = status;
|
||||
// if (projectId) filter.projectId = projectId;
|
||||
// if (vaId) filter.vaId = vaId;
|
||||
|
||||
if (status && status !== "undefined") filter.status = status;
|
||||
if (projectId && mongoose.Types.ObjectId.isValid(projectId)) {
|
||||
filter.projectId = new mongoose.Types.ObjectId(projectId);
|
||||
}
|
||||
if (vaId && mongoose.Types.ObjectId.isValid(vaId)) {
|
||||
filter.vaId = new mongoose.Types.ObjectId(vaId);
|
||||
}
|
||||
|
||||
const adjustments = await AdjustedHoursModel.find(filter)
|
||||
.sort({ createdAt: -1 })
|
||||
|
|
@ -54,9 +63,9 @@ async function get_adjustment_requests_list(req, res) {
|
|||
|
||||
} catch (error) {
|
||||
console.error("Get adjustment requests error:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
|
||||
import authFetch from "../../../utils/authFetch.js";
|
||||
|
||||
// Adjust missing hours by creating new records
|
||||
async function va_timesheet_adjustment_function(req, res) {
|
||||
|
|
@ -34,7 +35,7 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
// Validate project exists and VA is assigned to it
|
||||
const project = await ProjectModel.findOne({
|
||||
_id: projectId,
|
||||
vaAssigned: vaId
|
||||
"vaAssigned.vaId": vaId
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
|
|
@ -148,14 +149,14 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
"date": adjustmentDate.toISOString().split('T')[0],
|
||||
"loginTime": loginDateTime.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
|
||||
"status": "PENDING",
|
||||
"rejectionReason": "",
|
||||
"adjustedHours": formatTime(adjustedSeconds)
|
||||
}
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -167,6 +168,7 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
await AdjustedHoursModel.findByIdAndDelete(adjustmentRequest._id)
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
|
||||
import getDayNumber from "../../../utils/getDayNumber.js";
|
||||
|
||||
async function va_timesheet_summary_function(req, res) {
|
||||
try {
|
||||
const sf_token = req.headers.sf_token;
|
||||
const sf_token = req.headers["x-auth-token"];
|
||||
let vaId;
|
||||
let vaUser;
|
||||
let vaEmail;
|
||||
|
|
@ -43,7 +44,7 @@ async function va_timesheet_summary_function(req, res) {
|
|||
// Validate project exists and VA is assigned to it
|
||||
const project = await ProjectModel.findOne({
|
||||
_id: projectId,
|
||||
vaAssigned: vaId
|
||||
"vaAssigned.vaId": vaId
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
|
|
@ -162,8 +163,10 @@ async function va_timesheet_summary_function(req, res) {
|
|||
}
|
||||
};
|
||||
|
||||
const dayNumber = getDayNumber();
|
||||
|
||||
// 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 pendingHours = Math.max(0, dedicatedHours - totalWorkedHours);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js";
|
||||
import authFetch from "../../../utils/authFetch.js";
|
||||
|
||||
// Approve or reject leave
|
||||
async function va_tips_manage_function(req, res) {
|
||||
|
|
@ -38,7 +39,7 @@ async function va_tips_manage_function(req, res) {
|
|||
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
|
||||
}
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'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
|
||||
// }
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
const response = await authFetch(salesforceUrl, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logger from "../config/logger.js";
|
|||
|
||||
async function authValidator(req, res, next) {
|
||||
try {
|
||||
const sf_token = req.headers.sf_token;
|
||||
const sf_token = req.headers["x-auth-token"];
|
||||
if (sf_token) {
|
||||
const { vaId } = req.params;
|
||||
if(vaId) req.body.vaId = vaId;
|
||||
|
|
@ -50,6 +50,15 @@ async function authValidator(req, res, next) {
|
|||
|
||||
async function clientAuthValidator(req, res, next) {
|
||||
try {
|
||||
|
||||
const sf_token = req.headers["x-auth-token"];
|
||||
if (sf_token) {
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
if(sf_token) return;
|
||||
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
if (!accessToken) {
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ const projectSchema = new Schema({
|
|||
}
|
||||
],
|
||||
dedicatedHours: {
|
||||
type: String,
|
||||
default: "0"
|
||||
type: Array,
|
||||
default: ["0", "0", "0", "0", "0", "0", "0"]
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
|
|
@ -136,6 +136,12 @@ const clientDetailsSchema = new Schema({
|
|||
name: {
|
||||
type: String,
|
||||
},
|
||||
firstName: {
|
||||
type: String,
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -344,15 +350,6 @@ const vaWorkTrackSchema = new Schema({
|
|||
type: Types.ObjectId,
|
||||
ref: "TrackLog",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
appName: {
|
||||
type: String,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
},
|
||||
from: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
|
|
@ -365,6 +362,10 @@ const vaWorkTrackSchema = new Schema({
|
|||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
logs: {
|
||||
type: Array,
|
||||
default: [],
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js";
|
||||
import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js";
|
||||
import { clientDashboardGetVaLogsController } from "../../controllers/client/dashboard/clientDashboardGetVaLogs.controller.js";
|
||||
import { va_credits_get } from "../../controllers/client/dashboard/clientDashboardCredits.controller.js";
|
||||
|
||||
// clients routes
|
||||
clientRoutes.post("/register", va_register_client); // Register a new VA
|
||||
|
|
@ -28,6 +30,10 @@ clientRoutes.get("/tips", clientAuthValidator, va_tips_get);
|
|||
clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update);
|
||||
clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete);
|
||||
|
||||
clientRoutes.get("/credits", clientAuthValidator, va_credits_get);
|
||||
|
||||
clientRoutes.get("/vaLogsData/:vaId", clientAuthValidator, clientDashboardGetVaLogsController);
|
||||
|
||||
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
|
||||
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@ import { project_add_sf } from "../../controllers/project/projectAddSf.controlle
|
|||
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
|
||||
import { authValidator } from "../../middlewares/auth.middleware.js";
|
||||
import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js"
|
||||
import { createWireGuardConnection, disconnectWireGuardConnection } from '../../vpnHandler/vpn.js';
|
||||
|
||||
const v1Routes = express.Router()
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
v1Routes.get("/auth", function(req, res) {
|
||||
v1Routes.get("/auth", function (req, res) {
|
||||
res.sendFile(path.join(__dirname, '/auth.html'));
|
||||
});
|
||||
|
||||
|
|
@ -34,9 +35,50 @@ v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
|
|||
|
||||
v1Routes.post("/password/reset", process_reset);
|
||||
|
||||
v1Routes.get("/", function(req, res) {
|
||||
res.json({"connected": true})
|
||||
})
|
||||
v1Routes.get("/", function (req, res) {
|
||||
res.json({ "connected": true })
|
||||
})
|
||||
|
||||
v1Routes.post("/vpn/connect", (req, res) => {
|
||||
try {
|
||||
const data = createWireGuardConnection();
|
||||
res.json({
|
||||
success: true,
|
||||
message: "VPN connection created",
|
||||
...data
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
v1Routes.post("/vpn/disconnect", (req, res) => {
|
||||
try {
|
||||
const { ip } = req.body;
|
||||
if (!ip) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "IP address is required"
|
||||
});
|
||||
}
|
||||
|
||||
const result = disconnectWireGuardConnection(ip);
|
||||
if (result && result.success) {
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
|
||||
return res.status(400).json({success: false, error: result.message});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// v1Routes.use("/form_submit", authValidator, formRoutes);
|
||||
export default v1Routes;
|
||||
|
|
@ -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_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.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
|
||||
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
||||
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
||||
// vaRoutes.get("/timesheet/adjustments/list/: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);
|
||||
|
||||
// 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/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
|
||||
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
|
||||
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
|
||||
vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function);
|
||||
|
||||
// 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/track", authValidator, va_log_track_work_callback); // Track Window Tabs Logs Data
|
||||
vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time
|
||||
// vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time
|
||||
vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); // VA Audit Report // All Projects
|
||||
vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report
|
||||
// VA filter search: supports query params `name`, `email`, `from`, `to`
|
||||
|
|
@ -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("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip
|
||||
|
||||
|
||||
|
||||
|
||||
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
||||
|
||||
|
||||
|
|
|
|||
177
src/server.js
177
src/server.js
|
|
@ -144,19 +144,13 @@ app.get('/clientregister/:sf_token?', (req, res) => {
|
|||
|
||||
app.get('/auth/password/reset-form', render_reset_form);
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(201).send({ status: "Server is healthy" });
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
app.get("/client/login", (req, res) => {
|
||||
// res.render("client_login")
|
||||
res.render("client/client_login", {
|
||||
layout: "layouts/auth", // ⬅️ override layout
|
||||
title: "DocVA | Login"
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
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) => {
|
||||
try {
|
||||
// const dashboardData = await dashboardService.getClientDashboard(req.user.id);
|
||||
// res.render('client/dashboard-dashboard', dashboardData);
|
||||
// res.render('client/dashboard-dashboard');
|
||||
res.render('client/dashboard-dashboard', {
|
||||
title: "Client Dashboard",
|
||||
pageScript: "dashboard.js"
|
||||
});
|
||||
// res.render('layouts/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
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) => {
|
||||
try {
|
||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||||
// res.render('client/dashboard-virtual-assistants');
|
||||
res.render('client/dashboard-virtual-assistants', {
|
||||
title: "Virtual Assistants",
|
||||
pageScript:"dashboard-virtual-assistants.js"
|
||||
|
|
@ -199,6 +195,7 @@ app.get('/client/virtual-assistants', async (req, res) => {
|
|||
app.get('/client/projects', async (req, res) => {
|
||||
try {
|
||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||||
// res.render('client/dashboard-projects');
|
||||
res.render('client/dashboard-projects', {
|
||||
title: "Projects",
|
||||
pageScript:"projects.js"
|
||||
|
|
@ -213,10 +210,10 @@ app.get('/client/projects', async (req, res) => {
|
|||
app.get('/client/profile', async (req, res) => {
|
||||
try {
|
||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||||
// res.render('client/dashboard-profile');
|
||||
res.render('client/dashboard-profile',{
|
||||
title: "Profile",
|
||||
pageScript:"profile.js"
|
||||
|
||||
pageScript:"profile.js"
|
||||
});
|
||||
} catch (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) => {
|
||||
res.render('client/dashboard-manage-va-tips', {
|
||||
title: 'Manage VA Tips',
|
||||
pageScript: 'manage-tips.js'
|
||||
});
|
||||
app.get('/client/virtual-assistants/manage-tips', async (req, res) => {
|
||||
try {
|
||||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||||
// res.render('client/dashboard-manage-va-tips');
|
||||
res.render('client/dashboard-manage-va-tips', {
|
||||
title: 'Manage VA Tips',
|
||||
pageScript: 'manage-tips.js'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Projects error:', error);
|
||||
res.status(500).render('client/dashboard-manage-va-tips', { error: 'Failed to load profile' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/client/virtual-assistants/view-logs', async (req, res) => {
|
||||
try {
|
||||
// res.render('client/dashboard-va-logs');
|
||||
res.render('client/dashboard-va-logs', {
|
||||
title: 'VA Logs',
|
||||
pageScript: 'va-logs.js'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Projects error:', error);
|
||||
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/client/credits', async (req, res) => {
|
||||
try {
|
||||
// res.render('client/dashboard-va-logs');
|
||||
res.render('client/dashboard-credits', {
|
||||
title: 'Credits',
|
||||
pageScript: 'va-credits.js'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Projects error:', error);
|
||||
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -267,20 +297,20 @@ app.get("/live/:projectId/:vaId", async (req, res) => {
|
|||
// app.get("/live/:projectId/:vaId", async (req, res) => {
|
||||
// const { projectId, vaId } = req.params;
|
||||
|
||||
// if (!projectId || !vaId) {
|
||||
// return res.status(400).send({ "message": "Page not found" });
|
||||
// }
|
||||
// if (!projectId || !vaId) {
|
||||
// return res.status(400).send({ "message": "Page not found" });
|
||||
// }
|
||||
|
||||
// let clientId = null;
|
||||
// try {
|
||||
// const clientRes = await ProjectModel.findById(projectId);
|
||||
// if (clientRes) {
|
||||
// clientId = clientRes.clientId.toString();
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("Error fetching client for project:", err.message);
|
||||
// return;
|
||||
// }
|
||||
// let clientId = null;
|
||||
// try {
|
||||
// const clientRes = await ProjectModel.findById(projectId);
|
||||
// if (clientRes) {
|
||||
// clientId = clientRes.clientId.toString();
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("Error fetching client for project:", err.message);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!clientId) return res.status(400).send({ "message": "Page not found" });
|
||||
|
||||
|
|
@ -362,6 +392,8 @@ io.use(async (socket, next) => {
|
|||
if (clientUser) {
|
||||
socket.data.role = "client";
|
||||
socket.data.clientId = clientUser._id.toString();
|
||||
socket.data.vaId = vaId.toString();
|
||||
socket.data.projectId = projectId;
|
||||
|
||||
// Get all VA IDs assigned to this client
|
||||
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
|
||||
|
|
@ -382,7 +414,7 @@ io.on("connection", (socket) => {
|
|||
console.log("User connected:", socket.id);
|
||||
|
||||
// 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") {
|
||||
socket.data.role = "va";
|
||||
socket.data.vaId = id;
|
||||
|
|
@ -391,23 +423,35 @@ io.on("connection", (socket) => {
|
|||
|
||||
if (role === "client") {
|
||||
socket.data.role = "client";
|
||||
socket.data.clientId = id;
|
||||
socket.data.clientId = clientId;
|
||||
|
||||
// 🧩 Get allowed VAs for this client
|
||||
const client = await ClientModel.findById(id)
|
||||
.populate("projectIds") // to access their projects
|
||||
.populate("vaIds"); // optional if you track directly
|
||||
const client = await ClientModel.findById(clientId);
|
||||
|
||||
// Collect all VA IDs from their projects
|
||||
const projectVAIds = new Set();
|
||||
for (const project of client.projectIds) {
|
||||
const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
|
||||
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
|
||||
const a = JSON.stringify(client);
|
||||
console.log(a, "client data for allowed VAs");
|
||||
if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
|
||||
const projectVAIds = new Set();
|
||||
|
||||
const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
|
||||
populated.vaAssigned.forEach((va) => projectVAIds.add(va.vaId.toString()));
|
||||
|
||||
socket.data.allowedVAs = Array.from(projectVAIds);
|
||||
console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
|
||||
|
||||
io.to(vaSocketRespectToClient.get(`${clientId}-${id}-${projectId}-va`)).emit("startSendAppsDetails", { projId: projectId });
|
||||
}
|
||||
|
||||
// Store allowed VA IDs in socket memory
|
||||
socket.data.allowedVAs = Array.from(projectVAIds);
|
||||
console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs);
|
||||
// Collect all VA IDs from their projects
|
||||
// const projectVAIds = new Set();
|
||||
// for (const project of client.projectIds) {
|
||||
// const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
|
||||
// populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
|
||||
// }
|
||||
|
||||
// // 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();
|
||||
console.log("🟢 Received Apps from VA:", vaId);
|
||||
|
||||
// Broadcast image only to allowed clients
|
||||
for (let [_, clientSocket] of io.sockets.sockets) {
|
||||
|
|
@ -538,15 +581,58 @@ io.on("connection", (socket) => {
|
|||
);
|
||||
|
||||
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) => {
|
||||
res.status(404).render("not_found", {
|
||||
layout: "layouts/error",
|
||||
|
|
@ -554,7 +640,6 @@ app.use((req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
server.listen(PORT, () => {
|
||||
mongoose
|
||||
.connect(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import bcrypt from "bcrypt";
|
|||
import { VAModel, ClientModel } from "../models/vaModel.js";
|
||||
import logger from "../config/logger.js";
|
||||
import axios from "axios";
|
||||
import authFetch from "../utils/authFetch.js";
|
||||
|
||||
class PasswordResetService {
|
||||
constructor(passwordResetRepository) {
|
||||
|
|
@ -63,7 +64,7 @@ class PasswordResetService {
|
|||
logger.info(`Password reset token created for ${userType}: ${email}`);
|
||||
return {
|
||||
success: true,
|
||||
message: "If the email exists, a reset link has been sent"
|
||||
message: "Email sent successfully. Please check your inbox."
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -169,14 +170,19 @@ class PasswordResetService {
|
|||
try {
|
||||
const userTypeText = userType === 'va' ? 'VA' : 'CLIENT';
|
||||
|
||||
await axios.post(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, {
|
||||
data: {
|
||||
email: email,
|
||||
url: resetLink,
|
||||
userType: userTypeText
|
||||
}
|
||||
const res = await authFetch(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
email: email,
|
||||
url: resetLink,
|
||||
userType: userTypeText
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`);
|
||||
throw new Error("Failed to send reset email");
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
import axios from "axios";
|
||||
import logger from "../config/logger.js";
|
||||
import authFetch from "../utils/authFetch.js";
|
||||
|
||||
class SalesforceService {
|
||||
|
||||
async registerClientInSalesforce(clientData, _id) {
|
||||
try {
|
||||
const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`;
|
||||
|
||||
// Extract only the required data for Salesforce
|
||||
const salesforcePayload = this.prepareSalesforcePayload(clientData, _id);
|
||||
|
||||
logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`);
|
||||
|
||||
const response = await axios.post(url, salesforcePayload, {
|
||||
const response = await authFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(salesforcePayload),
|
||||
timeout: 30000, // 30 seconds timeout
|
||||
});
|
||||
|
||||
|
||||
logger.info(`SalesforceService - Client registered successfully in Salesforce: ${clientData.userDetails.email}`);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`);
|
||||
|
||||
|
||||
if (error.response) {
|
||||
// Salesforce returned an error response
|
||||
logger.error(`SalesforceService - Salesforce API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
|
||||
|
|
@ -41,11 +43,13 @@ class SalesforceService {
|
|||
|
||||
prepareSalesforcePayload(clientData, _id) {
|
||||
const { userDetails } = clientData;
|
||||
|
||||
|
||||
return {
|
||||
userDetails: {
|
||||
_id: _id,
|
||||
name: userDetails.name,
|
||||
firstName: userDetails.firstName,
|
||||
lastName: userDetails.lastName,
|
||||
email: userDetails.email.toLowerCase(),
|
||||
address: userDetails.address || null,
|
||||
city: userDetails.city || null,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class VaClientService {
|
|||
}
|
||||
|
||||
// Sanitize and prepare data
|
||||
const clientData = this.sanitizeClientData(data);
|
||||
const clientData = await this.sanitizeClientData(data);
|
||||
const response = await this.clientRepository.create(clientData);
|
||||
|
||||
if(response) {
|
||||
|
|
@ -75,7 +75,7 @@ class VaClientService {
|
|||
|
||||
// Define allowed fields for update (excluding password)
|
||||
const allowedFields = [
|
||||
"name", "address", "city", "state", "zip",
|
||||
"name", "firstName", "lastName", "address", "city", "state", "zip",
|
||||
"billingName", "billingEmail", "billingAddress", "billingCity",
|
||||
"billingState", "billingZip", "position", "signatoryName",
|
||||
"signatoryEmail", "paymentMethod", "bankName", "accountNumber",
|
||||
|
|
@ -263,13 +263,19 @@ class VaClientService {
|
|||
}
|
||||
|
||||
// Helper method to sanitize data
|
||||
sanitizeClientData(payload) {
|
||||
async sanitizeClientData(payload) {
|
||||
const { userDetails } = payload;
|
||||
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
|
||||
|
||||
return {
|
||||
userDetails: {
|
||||
firstName: userDetails.firstName,
|
||||
lastName: userDetails.lastName,
|
||||
name: userDetails.name,
|
||||
email: userDetails.email.toLowerCase(),
|
||||
password: hashedPassword,
|
||||
address: userDetails.address || null,
|
||||
city: userDetails.city || null,
|
||||
state: userDetails.state || null,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const convertToObjectId = (id) => {
|
||||
return mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : id;
|
||||
}
|
||||
|
||||
export default convertToObjectId;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
if (userType === 'client') {
|
||||
forgotPasswordUrl = '/client/forgot-password';
|
||||
loginUrl = '/client/login';
|
||||
loginUrl = '/';
|
||||
}
|
||||
%>
|
||||
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
<%
|
||||
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
|
||||
if (userType === 'client') {
|
||||
backToLoginUrl = '/client/login';
|
||||
backToLoginUrl = '/';
|
||||
}
|
||||
%>
|
||||
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<%
|
||||
let loginUrl = `${process.env.FRONTEND_URL}/login`;
|
||||
if (userType === 'client') {
|
||||
loginUrl = '/client/login';
|
||||
loginUrl = '/';
|
||||
}
|
||||
%>
|
||||
<a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a>
|
||||
|
|
|
|||
|
|
@ -282,12 +282,12 @@
|
|||
}
|
||||
|
||||
if (data.success) {
|
||||
// 🟢 Save token
|
||||
localStorage.setItem("knoptpq", data.accessToken);
|
||||
// localStorage.setItem("refreshToken", data.refreshToken);
|
||||
|
||||
// 🟢 Redirect
|
||||
window.location.href = "/client/dashboard";
|
||||
if(data.userType === "CLIENT") {
|
||||
localStorage.setItem("knoptpq", data.accessToken);
|
||||
window.location.href = "/client/dashboard";
|
||||
} else {
|
||||
window.location.href = "/download-tracker";
|
||||
}
|
||||
} else {
|
||||
alert("No token received. Please try again.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
|
|
@ -36,8 +35,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Virtual Assistants List -->
|
||||
<div class="content-section">
|
||||
<div class="section-header">
|
||||
|
|
@ -69,4 +66,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const tnyk = "<%= typeof sf_token !== 'undefined' ? sf_token : '' %>";
|
||||
if (tnyk) {
|
||||
localStorage.setItem('knoptpq', tnyk);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -23,7 +23,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Virtual Assistants List -->
|
||||
<div class="content-section">
|
||||
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@
|
|||
<!-- Password Change Section -->
|
||||
<div class="profile-section">
|
||||
<div class="password-section">
|
||||
<button class="change-password-btn" id="updateBankDetailsBtn">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
Update Bank Details
|
||||
</button>
|
||||
<button class="change-password-btn" id="changePasswordBtn">
|
||||
<i class="fas fa-lock"></i>
|
||||
Change Password
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-info">
|
||||
|
|
@ -35,7 +34,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Virtual Assistants List -->
|
||||
<div class="content-section">
|
||||
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
||||
|
|
@ -68,4 +66,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,12 +171,20 @@
|
|||
|
||||
// -------- Download --------
|
||||
downloadBtn.addEventListener("click", () => {
|
||||
const activeBtn = document.querySelector(".os-btn.active")
|
||||
const os = activeBtn.dataset.os;
|
||||
|
||||
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 = ``;
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
<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">
|
||||
<h2>Reset your password</h2>
|
||||
<input type="email" placeholder="Enter your email address*" required />
|
||||
|
|
@ -10,6 +12,24 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const root = document.documentElement;
|
||||
|
||||
function setTheme(theme) {
|
||||
document.getElementById("logo-img").src =
|
||||
theme === "dark"
|
||||
? "/public/images/logo-white-vertical.svg"
|
||||
: "/public/images/logo-colored-vertical.svg";
|
||||
root.setAttribute("data-theme", theme);
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
setTheme(getSystemTheme());
|
||||
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(email);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<%- body %>
|
||||
|
||||
<%- body %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>
|
||||
<%= title || 'Dashboard' %>
|
||||
</title>
|
||||
|
|
@ -33,6 +34,8 @@
|
|||
|
||||
<%- include('../partials/password-modal') %>
|
||||
|
||||
<%- include('../partials/bankDetails-modal') %>
|
||||
|
||||
<!-- COMMON SIDEBAR + LOGOUT -->
|
||||
<script src="/public/js/common/sidebar.js"></script>
|
||||
<script src="/public/js/common/header.js"></script>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
||||
<title>DocVA | Reset Password</title>
|
||||
|
||||
<style>
|
||||
|
|
@ -112,6 +113,17 @@
|
|||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 170px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
||||
<title>Client Registration</title>
|
||||
|
||||
<link href="/public/css/va-register.css" rel="stylesheet">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/public/images/favicon.svg" sizes="any">
|
||||
<title>Virtual Assistant Registration</title>
|
||||
|
||||
<link href="/public/css/va-register.css" rel="stylesheet">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
<div id="logoutModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Confirm Logout</h2>
|
||||
<button class="modal-close" onclick="closeLogoutModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="logout-modal-body">
|
||||
<div class="logout-icon">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</div>
|
||||
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
||||
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
|
||||
<div class="logout-actions">
|
||||
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
||||
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Confirm Logout</h2>
|
||||
<button class="modal-close" onclick="closeLogoutModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="logout-modal-body">
|
||||
<div class="logout-icon">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
||||
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
|
||||
<div class="logout-actions">
|
||||
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
||||
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,33 +1,33 @@
|
|||
<div id="passwordModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Change Password</h2>
|
||||
<button class="modal-close" onclick="closePasswordModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="passwordForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Current Password</label>
|
||||
<input type="password" class="form-input" id="currentPassword" required>
|
||||
<div class="error-message" id="currentPasswordError"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">New Password</label>
|
||||
<input type="password" class="form-input" id="newPassword" required>
|
||||
<div class="error-message" id="newPasswordError"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Confirm New Password</label>
|
||||
<input type="password" class="form-input" id="confirmPassword" required>
|
||||
<div class="error-message" id="confirmPasswordError"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="cancel-btn" onclick="closePasswordModal()">Cancel</button>
|
||||
<button type="submit" class="save-btn" id="savePasswordBtn">Change Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Change Password</h2>
|
||||
<button class="modal-close" onclick="closePasswordModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="passwordForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Current Password</label>
|
||||
<input type="password" class="form-input" id="currentPassword" required>
|
||||
<div class="error-message" id="currentPasswordError"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">New Password</label>
|
||||
<input type="password" class="form-input" id="newPassword" required>
|
||||
<div class="error-message" id="newPasswordError"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Confirm New Password</label>
|
||||
<input type="password" class="form-input" id="confirmPassword" required>
|
||||
<div class="error-message" id="confirmPasswordError"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="cancel-btn" onclick="closePasswordModal()">Cancel</button>
|
||||
<button type="submit" class="save-btn" id="savePasswordBtn">Change Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,39 +1,39 @@
|
|||
<div id="projectModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
|
||||
<h2 class="modal-title" id="modalProjectName">Project Details</h2>
|
||||
<button class="modal-close" id="modalClose">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="va-details-section">
|
||||
<h3 class="va-section-title">
|
||||
<i class="fas fa-users"></i>
|
||||
Virtual Assistants
|
||||
<span class="va-count-badge" id="vaCountBadge">0</span>
|
||||
</h3>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
|
||||
<h2 class="modal-title" id="modalProjectName">Project Details</h2>
|
||||
<button class="modal-close" id="modalClose">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="va-details-section">
|
||||
<h3 class="va-section-title">
|
||||
<i class="fas fa-users"></i>
|
||||
Virtual Assistants
|
||||
<span class="va-count-badge" id="vaCountBadge">0</span>
|
||||
</h3>
|
||||
|
||||
<div class="va-table-container">
|
||||
<table class="va-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Virtual Assistant</th>
|
||||
<th>Status</th>
|
||||
<th>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="modalVATableBody">
|
||||
<!-- VA details will be loaded here -->
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="modalNoVAs" class="no-vas-message" style="display: none;">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
<h4>No Virtual Assistants</h4>
|
||||
<p>No VAs are currently assigned to this project.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="va-table-container">
|
||||
<table class="va-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Virtual Assistant</th>
|
||||
<th>Status</th>
|
||||
<th>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="modalVATableBody">
|
||||
<!-- VA details will be loaded here -->
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="modalNoVAs" class="no-vas-message" style="display: none;">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
<h4>No Virtual Assistants</h4>
|
||||
<p>No VAs are currently assigned to this project.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<li>
|
||||
<a href="/client/projects" class="sidebar-link">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
<span>Projects</span>
|
||||
|
|
@ -33,6 +33,13 @@
|
|||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<a href="/client/credits" class="sidebar-link">
|
||||
<i class="fas fa-wallet"></i>
|
||||
<span>Credits</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<a href="javascript:void(0)" id="logoutButton" class="sidebar-link logout">
|
||||
|
|
@ -43,4 +50,4 @@
|
|||
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
</aside>
|
||||
|
|
@ -1,36 +1,36 @@
|
|||
<div id="tipModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Tip Virtual Assistant</h2>
|
||||
<button class="modal-close" onclick="closeTipModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Tip Virtual Assistant</h2>
|
||||
<button class="modal-close" onclick="closeTipModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="tipForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Select Virtual Assistant</label>
|
||||
<select name="vaForTip" class="form-input" id="vaForTip" required>
|
||||
<option value="">-- Select Virtual Assistant --</option>
|
||||
<!-- Options will be populated dynamically -->
|
||||
</select>
|
||||
<div class="error-message" id="vaForTipError"></div>
|
||||
</div>
|
||||
<div class="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 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>
|
||||
</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>
|
||||
|
|
@ -23,4 +23,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -86,14 +86,20 @@
|
|||
<form id="account-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 / Client Name *</label>
|
||||
<input type="text" id="first-name" class="input-field" placeholder=" " oninput="validateFirstName(this)" required>
|
||||
<label for="first-name" class="input-label">First 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 class="invalid-feedback" id="first-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||
First name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<input type="text" id="last-name" class="input-field" placeholder=" " oninput="validateLastName(this)" required>
|
||||
<label for="last-name" class="input-label">Last Name *</label>
|
||||
</div>
|
||||
<div class="invalid-feedback" id="last-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||
Last name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
@ -103,6 +109,24 @@
|
|||
<label for="email" class="input-label">Email *</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="new-password">
|
||||
<label for="password" class="input-label">Create Password *</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="new-password">
|
||||
<label for="confirmPassword" class="input-label">Confirm Password *</label>
|
||||
</div>
|
||||
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
|
||||
<div class="password-actions">
|
||||
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
|
||||
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<div></div> <!-- Empty div for spacing -->
|
||||
<button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
|
||||
|
|
@ -117,6 +141,15 @@
|
|||
<div class="error-message" id="step-2-error-message"></div>
|
||||
</div>
|
||||
<form id="business-form">
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<input type="text" id="company-name" class="input-field" placeholder=" " oninput="validateCompanyName(this)" required>
|
||||
<label for="company-name" class="input-label">Company Name *</label>
|
||||
</div>
|
||||
<div class="invalid-feedback" id="company-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||
Company name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<input type="text" id="address" class="input-field" placeholder=" " required>
|
||||
|
|
@ -246,8 +279,7 @@
|
|||
<div class="error-message" id="step-4-error-message"></div>
|
||||
</div>
|
||||
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
|
||||
We will use the details below in the agreement that we will send to your prior to entering the
|
||||
project.
|
||||
We will use the details below in the agreement that we will send to your prior to starting the project.
|
||||
</p>
|
||||
<form id="signatory-form">
|
||||
<div class="form-group">
|
||||
|
|
@ -341,7 +373,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="payment-option" onclick="selectPaymentOption('bank')">
|
||||
<h4>Bank Account (ACR)</h4>
|
||||
<h4>Bank Account (ACH)</h4>
|
||||
<p>Direct bank transfer</p>
|
||||
</div>
|
||||
<div id="bank-details" class="payment-details">
|
||||
|
|
|
|||
|
|
@ -240,23 +240,7 @@
|
|||
<div class="error-message" id="step-4-error-message"></div>
|
||||
</div>
|
||||
<form id="payment-form">
|
||||
<div class="form-group">
|
||||
<!-- <div class="payment-option" onclick="selectPaymentMethod('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="form-group">
|
||||
<div class="payment-option" onclick="selectPaymentMethod('veem')">
|
||||
<h4>Veem</h4>
|
||||
<p>Receive payments via Veem</p>
|
||||
|
|
@ -264,11 +248,11 @@
|
|||
<div id="veem-details" class="payment-details">
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<input type="text" id="veemAccount" class="input-field" placeholder=" " minlength="8" maxlength="25" oninput="validateVeemAccount(this)" onkeypress="return event.charCode >= 48 && event.charCode <= 57" required>
|
||||
<label for="veemAccount" class="input-label">Veem Account Number *</label>
|
||||
<input type="email" id="veemAccount" class="input-field" placeholder=" " oninput="validateVeemEmail(this)" required>
|
||||
<label for="veemAccount" class="input-label">Veem Email Address *</label>
|
||||
</div>
|
||||
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||
Account number must be 8-25 characters
|
||||
Invalid Veem email address
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -288,6 +272,23 @@
|
|||
</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 class="form-actions">
|
||||
|
|
|
|||
Loading…
Reference in New Issue