Compare commits

..

No commits in common. "320d44f4b58509b1671b0c6cfc3455570ee821e6" and "085e0c0e6dcc1e5efb96717711235225ac156210" have entirely different histories.

18 changed files with 268 additions and 550 deletions

View File

@ -670,13 +670,6 @@ body {
background-color: rgba(220, 38, 38, 0.1) !important; background-color: rgba(220, 38, 38, 0.1) !important;
} }
.additional-info-heading {
margin-top: 24px;
margin-bottom: 16px;
border-top: 1px solid #e5e7eb;
padding-top: 10px;
}
/* Animations */ /* Animations */
@keyframes slideDown { @keyframes slideDown {
from { from {

View File

@ -617,11 +617,11 @@
} else { } else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') { if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value; // const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) { // if (!payoneerAccount) {
showError('Please enter your Payoneer account number', 'payoneerAccount', step); // showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false; // isValid = false;
} // }
} else if (paymentMethod === 'veem') { } else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value; const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) { if (!veemAccount) {
@ -703,9 +703,9 @@
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
formData.userDetails.paymentMethod = paymentMethod; formData.userDetails.paymentMethod = paymentMethod;
if (document.getElementById('payoneerAccount').value) { // if (document.getElementById('payoneerAccount').value) {
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value; // formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
} // }
if (document.getElementById('veemAccount').value) { if (document.getElementById('veemAccount').value) {
formData.userDetails.veemAccount = document.getElementById('veemAccount').value; formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
} }
@ -734,7 +734,7 @@
// Show appropriate payment details // Show appropriate payment details
if (method === 'payoneer') { if (method === 'payoneer') {
document.getElementById('payoneer-details').style.display = 'block'; // document.getElementById('payoneer-details').style.display = 'block';
} else if (method === 'veem') { } else if (method === 'veem') {
document.getElementById('veem-details').style.display = 'block'; document.getElementById('veem-details').style.display = 'block';
} else if (method === 'wise') { } else if (method === 'wise') {

View File

@ -71,7 +71,7 @@ async function project_add_sf(req, res) {
hourlyRate: hourlyRate || '0', hourlyRate: hourlyRate || '0',
clientId: clientObjectId, clientId: clientObjectId,
vaAssigned: vaAssigned, vaAssigned: vaAssigned,
dedicatedHours: dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"], dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
vaWorkingScheduleStart: vaWorkingScheduleStart || null, vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null, vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowed: isFlexibleAllowed || true, isFlexibleAllowed: isFlexibleAllowed || true,

View File

@ -74,7 +74,7 @@ async function update_project_sf(req, res) {
// Update dedicated hours if provided // Update dedicated hours if provided
if (dedicatedHours !== undefined) { if (dedicatedHours !== undefined) {
updateData.dedicatedHours = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"]; updateData.dedicatedHours = parseFloat(dedicatedHours) || 0;
} }
// Update total logged hours if provided // Update total logged hours if provided

View File

@ -1,7 +1,6 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js"; import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// function to close open work tracks and create logout // function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog) { async function closePreviousSession(vaId, lastLog) {
@ -54,7 +53,7 @@ async function closePreviousSession(vaId, lastLog) {
"time": logoutLog.time, "time": logoutLog.time,
} }
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,6 +1,5 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js"; import { VAModel, LeavesModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Apply for leave // Apply for leave
async function va_leave_apply_function(req, res) { async function va_leave_apply_function(req, res) {
@ -165,7 +164,7 @@ async function va_leave_apply_function(req, res) {
"rejectionReason": "" "rejectionReason": ""
} }
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

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

View File

@ -1,5 +1,4 @@
import { VAModel } from "../../../models/vaModel.js"; import { VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
async function va_payslip_data_function(req, res) { async function va_payslip_data_function(req, res) {
try { try {
@ -12,7 +11,7 @@ async function va_payslip_data_function(req, res) {
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}`;
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

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

View File

@ -1,6 +1,5 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { VAModel } from "../../../models/vaModel.js"; import { VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Register a new VA // Register a new VA
async function va_register(req, res) { async function va_register(req, res) {
@ -54,7 +53,7 @@ async function va_register(req, res) {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
delete created.userDetails._id; delete created.userDetails._id;
created.userDetails._id = created._id; // Include VA's MongoDB ID created.userDetails._id = created._id; // Include VA's MongoDB ID
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,5 +1,4 @@
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js"; import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Helper function to get start and end of day in UTC // Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) { function getDayRange(date = new Date()) {
@ -198,7 +197,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
"time": logoutLog.time, "time": logoutLog.time,
} }
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -290,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: logId
});
// console.log(`Found work track data for logout:`, workTrackData);
if (workTrackData[0]) {
const fromLogTime = new Date(workTrackData[0].from);
const toLogTime = new Date(logTime);
const totalTimeMs = toLogTime - fromLogTime;
const updatedLogs = workTrackData[0].logs ? [...workTrackData[0].logs] : [];
if (updatedLogs.length > 0) {
const lastLogIndex = updatedLogs.length - 1;
const lastLogEntry = updatedLogs[lastLogIndex];
// Update the last log's 'to' time and recalculate its totalTime
const lastLogFromTime = new Date(lastLogEntry.from);
const lastLogTotalTime = Math.abs(toLogTime - lastLogFromTime);
updatedLogs[lastLogIndex] = {
...lastLogEntry,
to: toLogTime,
totalTime: lastLogTotalTime
};
// console.log(`Updated last log in array - from: ${lastLogFromTime.toISOString()}, to: ${toLogTime.toISOString()}, totalTime: ${lastLogTotalTime}ms`);
}
const updateResult = await WorkTrackModel.updateOne(
{ _id: workTrackData[0]._id },
{
$set: {
to: toLogTime,
totalTime: totalTimeMs,
logs: updatedLogs
}
}
);
console.log(`Updated work track ${workTrackData[0]._id}:`, {
from: fromLogTime.toISOString(),
to: toLogTime.toISOString(),
totalTime: totalTimeMs,
logsUpdated: updatedLogs.length,
mongodbUpdateResult: updateResult
});
// const updatedWorkTrack = await WorkTrackModel.findById(workTrackData[0]._id);
// console.log("Verified updated work track:", {
// to: updatedWorkTrack?.to,
// totalTime: updatedWorkTrack?.totalTime,
// logsCount: updatedWorkTrack?.logs?.length,
// lastLog: updatedWorkTrack?.logs?.[updatedWorkTrack.logs.length - 1]
// });
} else {
console.log(`No work track found for vaId: ${vaId}, projectId: ${projectId}, logId: ${logId}`);
}
// Create the logout log entry
const createdLog = await LoggedModel.create({
loggedType: type,
vaId,
vaEmail,
projectId,
time: logTime
});
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
"_id": createdLog._id,
"loggedType": type,
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": createdLog.time,
}
const response = await authFetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(sfData)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
return res.status(201).json({
success: true,
message: `${type} Successfully`,
logId: createdLog._id,
SyncSalesforce: result,
timeUsed: receivedTimeString ? 'received' : 'current',
actualTime: createdLog.time,
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Still return success since the logout was recorded locally
return res.status(201).json({
success: true,
message: `${type} Successfully (Salesforce sync failed)`,
logId: createdLog._id,
timeUsed: receivedTimeString ? 'received' : 'current',
actualTime: createdLog.time,
warning: "Salesforce sync failed"
});
}
}
// For LOGIN requests, check daily hours (using UTC day range) // For LOGIN requests, check daily hours (using UTC day range)
if (type === "LOGIN") { if (type === "LOGIN") {
const { start, end } = getDayRange(); // UTC day range const { start, end } = getDayRange(); // UTC day range
@ -472,7 +348,7 @@ async function va_project_log_callback(req, res) {
"time": createdLog.time, "time": createdLog.time,
} }
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -534,7 +410,7 @@ async function va_project_log_callback(req, res) {
"time": createdLog.time, "time": createdLog.time,
} }
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

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

View File

@ -1,6 +1,5 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js"; import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Adjust missing hours by creating new records // Adjust missing hours by creating new records
async function va_timesheet_adjustment_function(req, res) { async function va_timesheet_adjustment_function(req, res) {
@ -156,7 +155,7 @@ async function va_timesheet_adjustment_function(req, res) {
"adjustedHours": formatTime(adjustedSeconds) "adjustedHours": formatTime(adjustedSeconds)
} }
const response = await authFetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

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

View File

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

View File

@ -30,7 +30,7 @@ import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.cont
import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js"; import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js";
import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js"; import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA list vaRoutes.get("/", va_callback); // VA Callback for getting active VA
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
@ -76,7 +76,7 @@ vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_project
vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project
vaRoutes.post("/log/project/track", authValidator, va_log_track_work_callback); // Track Window Tabs Logs Data vaRoutes.post("/log/project/track", authValidator, va_log_track_work_callback); // Track Window Tabs Logs Data
// vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time vaRoutes.patch("/log/project/track", authValidator, va_log_track_work_callback); // Update Track Window Tab Closing Time
vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); // VA Audit Report // All Projects vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); // VA Audit Report // All Projects
vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report
// VA filter search: supports query params `name`, `email`, `from`, `to` // VA filter search: supports query params `name`, `email`, `from`, `to`

View File

@ -1,49 +0,0 @@
import fetch from "node-fetch"; // or global fetch in Node 18+
let options = {
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)
options.token = (await response.json()).access_token;
return options.token;
} catch (error) {
console.error('Error fetching access token:', error);
throw error;
}
};
export const authFetch = async (url, options = {}) => {
const token = await getAccessToken();
return fetch(url, {
...options,
headers: {
...(options.headers || {}),
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
};
export default authFetch;
export { getAccessToken }

View File

@ -262,6 +262,22 @@
</div> </div>
<form id="payment-form"> <form id="payment-form">
<div class="form-group"> <div class="form-group">
<!-- <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
</div>
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" onkeypress="return event.charCode >= 48 && event.charCode <= 57" required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
</div>
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 5-20 characters
</div>
</div>
</div> -->
<div class="payment-option" onclick="selectPaymentMethod('veem')"> <div class="payment-option" onclick="selectPaymentMethod('veem')">
<h4>Veem</h4> <h4>Veem</h4>
<p>Receive payments via Veem</p> <p>Receive payments via Veem</p>
@ -293,23 +309,6 @@
</div> </div>
</div> </div>
</div> </div>
<h2 class="form-title additional-info-heading">Additional Information</h2>
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
</div>
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="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> </div>
<div class="form-actions"> <div class="form-actions">