Compare commits

..

No commits in common. "master" and "ejs-ssr" have entirely different histories.

76 changed files with 989 additions and 4212 deletions

View File

29
package-lock.json generated
View File

@ -41,8 +41,7 @@
"uglify-js": "^3.19.3",
"uuid": "^11.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"wireguard-tools": "^0.1.0"
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@babel/cli": "^7.26.4",
@ -150,6 +149,7 @@
"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,6 +2361,7 @@
"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",
@ -2905,6 +2906,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@ -3742,7 +3744,8 @@
"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"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dom-serializer": {
"version": "0.1.1",
@ -6205,12 +6208,6 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -7999,6 +7996,7 @@
"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",
@ -8382,6 +8380,7 @@
"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"
@ -9090,6 +9089,7 @@
"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,6 +9137,7 @@
"integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.6.1",
"@webpack-cli/configtest": "^3.0.1",
@ -9302,6 +9303,7 @@
"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",
@ -9351,15 +9353,6 @@
"node": ">= 12.0.0"
}
},
"node_modules/wireguard-tools": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/wireguard-tools/-/wireguard-tools-0.1.0.tgz",
"integrity": "sha512-oBnnSB6wXGEAB1ZVPfbfpX8WlHNw7wv45r13ILX73felVnrGpMxMjFcKGAsMeRfBOGzXcW0LgAWe+T53dRcI+g==",
"license": "MIT",
"dependencies": {
"lodash.mergewith": "^4.6.2"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",

View File

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

View File

@ -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,6 +165,7 @@ body {
}
.mobile-menu-button {
display: none;
width: 2rem;
height: 2rem;
align-items: center;
@ -200,9 +201,6 @@ body {
.dashboard-sidebar {
display: block;
}
.mobile-menu-button {
display: none;
}
}
@media (max-width: 768px) {
@ -219,10 +217,6 @@ body {
.dashboard-sidebar.active {
transform: translateX(0);
}
.nav-list {
display: none;
}
}
.sidebar-header {
@ -314,6 +308,7 @@ select,
font-size: 14px;
}
[data-theme="dark"] .sidebar-link:hover {
background-color: var(--color-gray-800);
}
@ -631,7 +626,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 {
@ -999,17 +994,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;
}
@ -1269,7 +1264,6 @@ tr[data-vaid]:hover {
background: var(--color-gray-400);
}
@keyframes pulse {
0% {
opacity: 1;
@ -1679,11 +1673,11 @@ tr[data-vaid]:hover {
.form-grid {
grid-template-columns: 1fr;
}
.modal-actions {
flex-direction: column;
}
.logout-actions {
flex-direction: column;
}
@ -1701,39 +1695,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);
}
@ -1782,6 +1776,7 @@ tr[data-vaid]:hover {
transform: scale(1);
}
/* ================================
PROFILE DROPDOWN (GLASS STYLE)
================================ */
@ -1935,294 +1930,11 @@ 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);
}
}

View File

@ -670,13 +670,6 @@ 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 {

View File

@ -26,14 +26,4 @@ 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");
}
);
});

View File

@ -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>` : `<a class="va-logs va-action" data-va-id="${va.id}" target="_blank" href="/client/virtual-assistants/view-logs?va=${va.id}">Logs</a>`;
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>';
return `
<tr class="va-row" data-va-id="${va.id}">

File diff suppressed because it is too large Load Diff

View File

@ -1,61 +0,0 @@
// API base URL
const API_BASE = '/api/v1/client';
const vaCreditsTableBody = document.getElementById("vaCreditTableBody");
const vaCreditsEmptyState = document.getElementById("vaEmptyCreditState");
function createCreditRow(credit) {
return `
<tr data-credit-id="${credit.id}">
<td><span class="status ${credit.status.toLowerCase()}">${credit.status}</span></td>
<td>${credit.date}</td>
<td>${credit.comment}</td>
<td>$${credit.creditAmount}</td>
</tr>
`;
}
async function loadCreditsData() {
const res = await fetchWithAuth(`${API_BASE}/credits`);
if (!res) return;
const result = await res.json();
if (!result.success) return;
console.log("Credits Result:", result);
const { data } = result;
console.log("Credits Data:", data);
const allCredits = data || [];
if (allCredits.length === 0) {
vaCreditsTableBody.innerHTML = "";
vaCreditsEmptyState.style.display = "block";
return;
}
vaCreditsEmptyState.style.display = "none";
vaCreditsTableBody.innerHTML = allCredits
.map((credit) => createCreditRow(credit))
.join("");
let totalCredits = 0;
let availableCredits = 0;
allCredits.forEach(credit => {
totalCredits += parseFloat(credit.creditAmount);
if (credit.status.toLowerCase() === 'pending') {
availableCredits += parseFloat(credit.creditAmount);
}
});
document.getElementById("vaTotalCredits").textContent = `$${totalCredits}`;
document.getElementById("vaAvailableCredits").textContent = `$${availableCredits}`;
}
// Initial load
loadCreditsData();

View File

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

View File

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

View File

@ -617,22 +617,16 @@
} else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) {
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false;
} else if (!validateEmail(payoneerAccount)) {
showError('Please enter a valid email address for Payoneer', 'payoneerAccount', step);
isValid = false;
}
// const payoneerAccount = document.getElementById('payoneerAccount').value;
// if (!payoneerAccount) {
// showError('Please enter your Payoneer account number', 'payoneerAccount', step);
// isValid = false;
// }
} else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) {
showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false;
} 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;
@ -709,9 +703,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;
}
@ -740,7 +734,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') {
@ -769,48 +763,6 @@
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;

View File

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

View File

@ -1,40 +0,0 @@
import authFetch from "../../../utils/authFetch.js";
async function va_credits_get(req, res) {
try {
const clientId = req.body.clientId;
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/credit/${clientId}`;
const response = await authFetch(salesforceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({
success: true,
data: result.data
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
}
export { va_credits_get };

View File

@ -1,117 +0,0 @@
import { VAModel, ClientModel, ProjectModel, WorkTrackModel } from "../../../models/vaModel.js";
async function clientDashboardGetVaLogsController(req, res) {
try {
const clientId = req.body.clientId;
const clientUser = await ClientModel.findById(clientId);
if (!clientUser) {
return res.status(404).json({ success: false, message: "Client not found" });
}
const { vaId } = req.params;
const vaUser = await VAModel.findById(vaId);
if (!vaUser) {
return res.status(404).json({ success: false, message: "Virtual Assistant not found" });
}
// Get shared active projects
const sharedProjects = await ProjectModel.find({
clientId: clientUser._id,
'vaAssigned.vaId': vaId,
status: "ACTIVE"
}).select('name');
if (!sharedProjects || sharedProjects.length === 0) {
return res.status(200).json({
success: true,
message: "No shared projects found",
data: { projectLogs: [] }
});
}
const projectIds = sharedProjects.map(p => p._id);
// Get work tracks for the last 15 days
const fifteenDaysAgo = new Date();
fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15);
// Get all work tracks
const workTracks = await WorkTrackModel.find({
vaId: vaId,
projectId: { $in: projectIds },
from: { $gte: fifteenDaysAgo }
})
.sort({ from: -1 })
.lean();
// Build project logs array
const projectLogs = [];
let totalWorkTracks = 0;
let totalHours = 0;
sharedProjects.forEach(project => {
// Filter tracks for this project
const projectTracks = workTracks.filter(track =>
track.projectId.toString() === project._id.toString()
);
// Only add project if it has tracks
if (projectTracks.length > 0) {
// Calculate project total hours
const projectTotalHours = projectTracks.reduce((sum, track) => sum + (track.totalTime || 0), 0);
projectLogs.push({
projectDetails: {
id: project._id,
name: project.name,
hourlyRate: project.hourlyRate
},
workTrackLogs: projectTracks.map(track => ({
id: track._id,
from: track.from,
to: track.to,
totalTime: track.totalTime,
logs: track.logs || [],
createdAt: track.createdAt,
updatedAt: track.updatedAt
}))
});
totalWorkTracks += projectTracks.length;
totalHours += projectTotalHours;
}
});
const response = {
success: true,
message: "VA logs retrieved successfully",
data: {
projectLogs: projectLogs
}
};
// Add summary if needed (optional)
if (projectLogs.length > 0) {
response.data.summary = {
totalProjects: projectLogs.length,
totalWorkTracks: totalWorkTracks,
totalHours: totalHours,
dateRange: {
startDate: fifteenDaysAgo.toISOString().split('T')[0],
endDate: new Date().toISOString().split('T')[0]
}
};
}
return res.status(200).json(response);
} catch (error) {
console.error('Error fetching VA logs:', error);
return res.status(500).json({
success: false,
message: 'Failed to load VA logs'
});
}
}
export { clientDashboardGetVaLogsController };

View File

@ -1,6 +1,5 @@
import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken";
import getDayNumber from "../../../utils/getDayNumber.js";
function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
@ -35,6 +34,8 @@ 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" });
}
@ -113,15 +114,13 @@ 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[dayNumber]) || "0h",
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || "0h",
status: project.status || '',
assignedVADetails: validVADetails
};

View File

@ -2,67 +2,51 @@ import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.js";
import logger from "../../config/logger.js";
import jwt from "jsonwebtoken";
import PasswordResetService from "../../services/passwordReset.service.js";
import PasswordResetRepository from "../../repository/passwordReset.repository.js";
const vaClientService = new VaClientService(new VaClientRepository());
const passwordResetService = new PasswordResetService(
new PasswordResetRepository(),
// new EmailService({
// host: process.env.SMTP_HOST,
// port: process.env.SMTP_PORT,
// user: process.env.SMTP_USER,
// pass: process.env.SMTP_PASS,
// from: process.env.EMAIL_FROM,
// })
);
async function va_register_client(req, res) {
logger.info("ClientController - Register Client Hit ...");
try {
const payload = req.body || {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.lastName || !userDetails.password || !userDetails.name) {
return res.status(400).json({
success: false,
message: "Missing required fields: firstName, lastName, email, password, and company name are required"
if (!userDetails || !userDetails.email || !userDetails.name) {
return res.status(400).json({
success: false,
message: "Missing required fields: name and email are required"
});
}
const created = await vaClientService.registerClient(payload);
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
await passwordResetService.requestPasswordReset(userDetails.email, 'client');
logger.info(`Client password reset requested for: ${userDetails.email}`);
return res.status(201).json({
success: true,
return res.status(201).json({
success: true,
data: created,
message: "Client registered successfully"
message: "Client registered successfully"
});
} catch (error) {
logger.error(`ClientController - Register error: ${error.message}`);
if (error.message === "User already exists with this email") {
return res.status(409).json({
success: false,
message: error.message
return res.status(409).json({
success: false,
message: error.message
});
}
if (error.message.includes("Salesforce API is unreachable")) {
return res.status(503).json({
success: false,
message: "Salesforce service is currently unavailable. Please try again later."
return res.status(503).json({
success: false,
message: "Salesforce service is currently unavailable. Please try again later."
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error"
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
@ -72,52 +56,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,
@ -127,79 +111,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: error.message
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
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" });
}
@ -211,14 +195,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"
});
}
@ -233,15 +217,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"
});
}
}
@ -250,8 +234,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" });
}
@ -265,14 +249,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"
});
}
@ -282,41 +266,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,

View File

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

View File

@ -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.vaId } },
{ _id: { $in: vaAssigned } },
{ $pull: { projectIds: projectObjectId } }
);
deletionSummary.vasUpdated = vaResult.modifiedCount;

View File

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

View File

@ -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,
isFlexibleAllowed,
isFlexibleAllowedPt,
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 = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"];
updateData.dedicatedHours = parseFloat(dedicatedHours) || 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 (isFlexibleAllowed !== undefined) {
updateData.isFlexibleAllowed = isFlexibleAllowed;
if (isFlexibleAllowedPt !== undefined) {
updateData.isFlexibleAllowedPt = isFlexibleAllowedPt;
}
if (vaWorkingDelayAllowedStart !== undefined) {
@ -114,52 +114,41 @@ 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 = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
let newVaAssignedArray = [];
// Parse and validate VA IDs
if (vaAssigned && vaAssigned.length > 0) {
if (Array.isArray(vaAssigned)) {
try {
newVaAssignedArray = vaAssigned.map(vaId => {
if (typeof vaId === 'object' && "isFlexibleAllowed" in vaId) {
// Handle Error form SF Side for now - That vaId is coming as object
return new Types.ObjectId(vaId.vaId)
} else {
return new Types.ObjectId(vaId)
}
});
newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format in vaAssigned array"
return res.status(400).json({
success: false,
message: "Invalid VA ID format in vaAssigned array"
});
}
} else if (typeof vaAssigned === 'string') {
try {
newVaAssignedArray = [new Types.ObjectId(vaAssigned)];
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format"
return res.status(400).json({
success: false,
message: "Invalid VA ID format"
});
}
}
}
// updateData.vaAssigned = newVaAssignedArray;
updateData.vaAssigned = newVaAssignedArray;
// Calculate VAs to add and remove for relationship updates
const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.toString());
const currentVaIds = existingProject.vaAssigned.map(va => va.toString());
const newVaIds = newVaAssignedArray.map(va => va.toString());
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.vaId.toString()));
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString()));
// Update relationships for added VAs
if (vaToAdd.length > 0) {
@ -186,9 +175,9 @@ async function update_project_sf(req, res) {
// If no fields to update
if (Object.keys(updateData).length === 0) {
return res.status(400).json({
success: false,
message: "No valid fields to update"
return res.status(400).json({
success: false,
message: "No valid fields to update"
});
}
@ -198,7 +187,7 @@ async function update_project_sf(req, res) {
updateData,
{ new: true, runValidators: true }
).populate('clientId', 'userDetails.name userDetails.email')
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
// If VA assignments were changed but relationship update failed, log warning
if (vaAssigned !== undefined) {
@ -222,33 +211,33 @@ async function update_project_sf(req, res) {
} catch (error) {
console.error("update_project_sf error:", error);
// MongoDB duplicate key error
if (error.code === 11000) {
return res.status(400).json({
success: false,
message: "Project with this name already exists"
return res.status(400).json({
success: false,
message: "Project with this name already exists"
});
}
// MongoDB validation error
if (error.name === 'ValidationError') {
const errors = Object.values(error.errors).map(err => err.message);
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: "Validation error",
errors: errors
errors: errors
});
}
return res.status(500).json({
success: false,
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
error: error.message
});
}
}
export {
export {
update_project_sf
};

View File

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

View File

@ -1,6 +1,5 @@
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) {
@ -159,13 +158,13 @@ async function va_leave_apply_function(req, res) {
"dateTo": endDate.toISOString().split('T')[0],
"totalDays": totalDays,
"leaveType": leaveType,
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
"comment": processedComment.plainText, // Send plain text to Salesforce
"attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING",
"rejectionReason": ""
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -177,7 +176,6 @@ 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}`);
}

View File

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

View File

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

View File

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

View File

@ -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": vaId } }
{ $pull: { vaAssigned: vaId } }
);
projectsUpdated = projectResult.modifiedCount;
console.log(`Removed VA from ${projectsUpdated} projects`);

View File

@ -21,12 +21,16 @@ 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" });
}

View File

@ -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["x-auth-token"];
const sf_token = req.headers.sf_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", "wiseEmail"
"paymentMethod", "payoneerAccount", "veemAccount"
];
let hasUpdates = false;

View File

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

View File

@ -1,16 +1,13 @@
import mongoose from "mongoose";
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
import getDayNumber from "../../../utils/getDayNumber.js";
// 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 };
}
@ -18,7 +15,7 @@ function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} else if (wholeHours > 0) {
@ -31,11 +28,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');
@ -67,84 +64,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.isFlexibleAllowed) {
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) {
// 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 };
// }
@ -152,7 +149,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,
@ -163,22 +160,22 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
{ to: null }
]
});
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
// Close all open work tracks
for (const workTrack of openWorkTracks) {
const fromTime = new Date(workTrack.from);
const totalTimeMs = currentTime - fromTime;
// Update the work track with end time and total time
workTrack.to = currentTime;
workTrack.totalTime = totalTimeMs;
await workTrack.save();
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
}
// Create a logout log entry
const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT",
@ -187,20 +184,20 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
projectId: lastLog.projectId,
time: currentTime // Use same timestamp for consistent logging
});
// console.log(`Created logout log: ${logoutLog._id}`);
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId,
"time": logoutLog.time,
}
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId,
"time": logoutLog.time,
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -209,7 +206,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
},
body: JSON.stringify(sfDataSync)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
@ -217,7 +214,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
@ -226,13 +223,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;
@ -245,7 +242,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");
}
@ -253,11 +250,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;
@ -292,129 +289,6 @@ 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
@ -424,20 +298,18 @@ 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[dayNumber] > 0 && totalTodayHours >= project.dedicatedHours[dayNumber]) {
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
return res.status(400).json({
success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours[dayNumber])})`,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`,
// warnings: scheduleCheck.warnings
});
}
@ -457,10 +329,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
});
@ -468,15 +340,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 authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -485,7 +357,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}`);
@ -493,11 +365,11 @@ async function va_project_log_callback(req, res) {
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return res.status(201).json({
success: true,
message: `${type} Successfully`,
logId: createdLog._id,
res.status(201).json({
success: true,
message: `${type} Successfully`,
logId: createdLog._id,
SyncSalesforce: result,
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
@ -510,7 +382,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;
@ -519,10 +391,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
});
@ -530,15 +402,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 authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -547,7 +419,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}`);
@ -555,11 +427,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,
@ -568,7 +440,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
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) {
@ -39,7 +38,7 @@ async function va_tips_manage_function(req, res) {
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -117,7 +116,7 @@ async function va_tips_delete(req, res) {
// "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
// }
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',

View File

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

View File

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

View File

@ -14,8 +14,6 @@ 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
@ -30,10 +28,6 @@ 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)

View File

@ -13,14 +13,13 @@ 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'));
});
@ -35,50 +34,9 @@ v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
v1Routes.post("/password/reset", process_reset);
v1Routes.get("/", function (req, res) {
res.json({ "connected": true })
})
v1Routes.post("/vpn/connect", (req, res) => {
try {
const data = createWireGuardConnection();
res.json({
success: true,
message: "VPN connection created",
...data
});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
});
v1Routes.post("/vpn/disconnect", (req, res) => {
try {
const { ip } = req.body;
if (!ip) {
return res.status(400).json({
success: false,
error: "IP address is required"
});
}
const result = disconnectWireGuardConnection(ip);
if (result && result.success) {
return res.status(200).json(result);
}
return res.status(400).json({success: false, error: result.message});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
});
v1Routes.get("/", function(req, res) {
res.json({"connected": true})
})
// v1Routes.use("/form_submit", authValidator, formRoutes);
export default v1Routes;

View File

@ -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 list
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
@ -47,9 +47,7 @@ vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
// timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", get_adjustment_requests_list); // show all adjustment list to approve and reject
// vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
// Leaves routes
@ -58,7 +56,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", authValidator, va_leave_manage_function);
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes
@ -76,7 +74,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`
@ -86,6 +84,9 @@ 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

View File

@ -144,13 +144,19 @@ 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) => {
@ -164,12 +170,11 @@ 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' });
@ -179,7 +184,6 @@ 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"
@ -195,7 +199,6 @@ 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"
@ -210,10 +213,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);
@ -221,44 +224,11 @@ app.get('/client/profile', async (req, res) => {
}
});
app.get('/client/virtual-assistants/manage-tips', async (req, res) => {
try {
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
// res.render('client/dashboard-manage-va-tips');
res.render('client/dashboard-manage-va-tips', {
title: 'Manage VA Tips',
pageScript: 'manage-tips.js'
});
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-manage-va-tips', { error: 'Failed to load profile' });
}
});
app.get('/client/virtual-assistants/view-logs', async (req, res) => {
try {
// res.render('client/dashboard-va-logs');
res.render('client/dashboard-va-logs', {
title: 'VA Logs',
pageScript: 'va-logs.js'
});
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
}
});
app.get('/client/credits', async (req, res) => {
try {
// res.render('client/dashboard-va-logs');
res.render('client/dashboard-credits', {
title: 'Credits',
pageScript: 'va-credits.js'
});
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
}
app.get('/client/virtual-assistants/manage-tips', (req, res) => {
res.render('client/dashboard-manage-va-tips', {
title: 'Manage VA Tips',
pageScript: 'manage-tips.js'
});
});
@ -297,20 +267,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" });
@ -392,8 +362,6 @@ io.use(async (socket, next) => {
if (clientUser) {
socket.data.role = "client";
socket.data.clientId = clientUser._id.toString();
socket.data.vaId = vaId.toString();
socket.data.projectId = projectId;
// Get all VA IDs assigned to this client
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
@ -414,7 +382,7 @@ io.on("connection", (socket) => {
console.log("User connected:", socket.id);
// Register role and ID (client or VA)
socket.on("register", async ({ role, id, projectId = null, clientId = null }) => {
socket.on("register", async ({ role, id }) => {
if (role === "va") {
socket.data.role = "va";
socket.data.vaId = id;
@ -423,35 +391,23 @@ io.on("connection", (socket) => {
if (role === "client") {
socket.data.role = "client";
socket.data.clientId = clientId;
socket.data.clientId = id;
// 🧩 Get allowed VAs for this client
const client = await ClientModel.findById(clientId);
const a = JSON.stringify(client);
console.log(a, "client data for allowed VAs");
if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
const projectVAIds = new Set();
const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
populated.vaAssigned.forEach((va) => projectVAIds.add(va.vaId.toString()));
socket.data.allowedVAs = Array.from(projectVAIds);
console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
io.to(vaSocketRespectToClient.get(`${clientId}-${id}-${projectId}-va`)).emit("startSendAppsDetails", { projId: projectId });
}
const client = await ClientModel.findById(id)
.populate("projectIds") // to access their projects
.populate("vaIds"); // optional if you track directly
// Collect all VA IDs from their projects
// const projectVAIds = new Set();
// for (const project of client.projectIds) {
// const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
// populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
// }
const projectVAIds = new Set();
for (const project of client.projectIds) {
const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
}
// // Store allowed VA IDs in socket memory
// socket.data.allowedVAs = Array.from(projectVAIds);
// console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
// Store allowed VA IDs in socket memory
socket.data.allowedVAs = Array.from(projectVAIds);
console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs);
}
});
@ -551,6 +507,7 @@ 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) {
@ -581,58 +538,15 @@ io.on("connection", (socket) => {
);
if (vaSocketId) {
// io.to(vaSocketId).emit("stopSendAppsDetails");
io.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("stopSendAppsDetails", { projId: projectId });
io.to(vaSocketId).emit("stopSendAppsDetails");
}
}
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",
@ -640,6 +554,7 @@ app.use((req, res) => {
});
});
server.listen(PORT, () => {
mongoose
.connect(

View File

@ -3,7 +3,6 @@ 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) {
@ -64,7 +63,7 @@ class PasswordResetService {
logger.info(`Password reset token created for ${userType}: ${email}`);
return {
success: true,
message: "Email sent successfully. Please check your inbox."
message: "If the email exists, a reset link has been sent"
};
} catch (error) {
@ -170,19 +169,14 @@ class PasswordResetService {
try {
const userTypeText = userType === 'va' ? 'VA' : 'CLIENT';
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
}
})
await axios.post(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, {
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");

View File

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

View File

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

View File

@ -1,68 +0,0 @@
import fetch from "node-fetch"; // or global fetch in Node 18+
let preOptions = {
token: ''
}
const getAccessToken = async () => {
try {
const response = await fetch(`${process.env.SF_DOCTRACKER_API_URL}services/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SF_CLIENT_ID,
client_secret: process.env.SF_CLIENT_SECRET,
}),
});
console.log(response)
const access_token = (await response.json()).access_token;
preOptions.token = access_token;
return access_token;
} catch (error) {
console.error('Error fetching access token:', error);
throw error;
}
};
export const authFetch = async (url, options = {}) => {
let isvalid = false;
if (preOptions.token != "" && preOptions.token != null && preOptions.token != undefined) {
isvalid = await fetch(`${process.env.SF_DOCTRACKER_TOKEN_VALIDATION_URL}services/apexrest/docva/health}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${preOptions.token}`,
},
});
const a = JSON.stringify(isvalid.body);
console.log(a);
}
if (isvalid.status !== 200) {
await getAccessToken();
}
return fetch(url, {
...options,
headers: {
...(options.headers || {}),
"Content-Type": "application/json",
Authorization: `Bearer ${preOptions.token}`,
},
});
};
export default authFetch;
export { getAccessToken }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Total Credits</h3>
<div class="stat-value" id="vaTotalCredits">$0</div>
</div>
<div class="stat-icon blue">
<i class="fas fa-dollar fa-xl"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Available Credits</h3>
<div class="stat-value" id="vaAvailableCredits">$0</div>
</div>
<div class="stat-icon blue">
<i class="fas fa-dollar fa-xl"></i>
</div>
</div>
</div>
</div>
<!-- Virtual Assistants List -->
<div class="content-section">
<div class="table-container">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Status</th>
<th>Date</th>
<th>Description</th>
<th>Amount</th>
</tr>
</thead>
<tbody id="vaCreditTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div id="vaEmptyCreditState" style="display: none;">
<div class="empty-state">
<i class="fas fa-users"></i>
<h3>No Credit Found</h3>
<p>You haven't received any credit yet.</p>
</div>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
@ -35,6 +36,8 @@
</div>
</div>
</div>
<!-- Virtual Assistants List -->
<div class="content-section">
<div class="section-header">
@ -66,11 +69,4 @@
</div>
</div>
</div>
</div>
<script>
const tnyk = "<%= typeof sf_token !== 'undefined' ? sf_token : '' %>";
if (tnyk) {
localStorage.setItem('knoptpq', tnyk);
}
</script>
</div>

View File

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

View File

@ -55,10 +55,6 @@
<!-- 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

View File

@ -1,25 +0,0 @@
<!-- Virtual Assistants List -->
<div class="content-section">
<div class="section-header" style="justify-content: space-between;align-items: center;">
<a href="/client/virtual-assistants" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to VAs
</a>
</div>
<!-- Loading State -->
<div id="loadingState" class="loading-spinner">
<div class="spinner"></div>
<p>Loading VA logs...</p>
</div>
<!-- Empty State -->
<div id="emptyState" class="empty-state" style="display: none;">
<i class="fas fa-clipboard-list"></i>
<h3>No Logs Found</h3>
<p>This virtual assistant hasn't logged any time in the last 15 days.</p>
</div>
<!-- Projects Accordion Container -->
<div id="projectsContainer" style="display: none;"></div>
</div>

View File

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

View File

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

View File

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

View File

@ -11,7 +11,8 @@
</head>
<body>
<%- body %>
<%- body %>
</body>
</html>

View File

@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
<%= title || 'Dashboard' %>
</title>
@ -34,8 +33,6 @@
<%- 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>

View File

@ -4,7 +4,6 @@
<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>
@ -113,17 +112,6 @@
opacity: 1;
text-decoration: underline;
}
.logo {
text-align: center;
margin-bottom: 20px;
margin-inline: auto;
}
.logo img {
width: 170px;
height: auto;
}
</style>
</head>

View File

@ -4,7 +4,6 @@
<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">

View File

@ -3,7 +3,6 @@
<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">

View File

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

View File

@ -1,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 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>
<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>
</div>

View File

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

View File

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

View File

@ -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,13 +33,6 @@
</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">
@ -50,4 +43,4 @@
</ul>
</nav>
</aside>
</aside>

View File

@ -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>
<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 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="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 class="modal-body">
<form class="modal-form" id="tipForm">
<div class="form-group">
<label class="form-label">Select Virtual Assistant</label>
<select name="vaForTip" class="form-input" id="vaForTip" required>
<option value="">-- Select Virtual Assistant --</option>
<!-- Options will be populated dynamically -->
</select>
<div class="error-message" id="vaForTipError"></div>
</div>
<div class="form-group">
<label class="form-label">Tip Amount</label>
<input type="text" class="form-input" id="vaTipAmount" required>
<div class="error-message" id="vaTipAmountError"></div>
</div>
<div class="form-group">
<label class="form-label">Tip Description</label>
<textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
<div class="error-message" id="vaTipDescriptionError"></div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
</div>
</form>
</div>
<div class="form-group">
<label class="form-label">Tip Description</label>
<textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
<div class="error-message" id="vaTipDescriptionError"></div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@ -23,4 +23,4 @@
</div>
</div>
</div>
</div>
</div>

View File

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

View File

@ -240,7 +240,23 @@
<div class="error-message" id="step-4-error-message"></div>
</div>
<form id="payment-form">
<div class="form-group">
<div class="form-group">
<!-- <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
</div>
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" onkeypress="return event.charCode >= 48 && event.charCode <= 57" required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
</div>
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 5-20 characters
</div>
</div>
</div> -->
<div class="payment-option" onclick="selectPaymentMethod('veem')">
<h4>Veem</h4>
<p>Receive payments via Veem</p>
@ -248,11 +264,11 @@
<div id="veem-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="email" id="veemAccount" class="input-field" placeholder=" " oninput="validateVeemEmail(this)" required>
<label for="veemAccount" class="input-label">Veem Email Address *</label>
<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>
</div>
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Invalid Veem email address
Account number must be 8-25 characters
</div>
</div>
</div>
@ -272,23 +288,6 @@
</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">