Updated track logs api

This commit is contained in:
Aakash Mittal 2025-12-15 13:40:36 +05:30
parent 4ed62b25e8
commit 645adea832
5 changed files with 214 additions and 209 deletions

View File

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

View File

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

@ -26,140 +26,147 @@ function formatHoursToReadable(hours) {
} }
} }
// Helper function to check daily hours and create logout log if needed // // Helper function to check daily hours and create logout log if needed
async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) { // async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
try { // try {
const today = new Date(); // const today = new Date();
const { start, end } = getDayRange(today); // const { start, end } = getDayRange(today);
// Get today's work entries for this VA and project // // Get today's work entries for this VA and project
// Exclude the current session if it's already in the database // // Exclude the current session if it's already in the database
const query = { // const query = {
vaId, // vaId,
projectId, // projectId,
from: { $gte: start, $lte: end } // from: { $gte: start, $lte: end }
}; // };
if (currentSessionId) { // if (currentSessionId) {
query._id = { $ne: currentSessionId }; // query._id = { $ne: currentSessionId };
} // }
const todayWorkEntries = await WorkTrackModel.find(query); // const todayWorkEntries = await WorkTrackModel.find(query);
// Calculate total hours worked today // // Calculate total hours worked today
let totalTodayMs = todayWorkEntries.reduce((total, entry) => { // let totalTodayMs = todayWorkEntries.reduce((total, entry) => {
return total + (entry.totalTime || 0); // return total + (entry.totalTime || 0);
}, 0); // }, 0);
// Add the additional time from current session if provided // // Add the additional time from current session if provided
totalTodayMs += additionalTimeMs; // totalTodayMs += additionalTimeMs;
const totalTodayHours = totalTodayMs / (1000 * 60 * 60); // const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
// Check if daily dedicated hours are completed // // Check if daily dedicated hours are completed
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) { // if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
// Create automatic logout log // // Create automatic logout log
const logoutLog = await LoggedModel.create({ // const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT", // loggedType: "LOGOUT",
vaId, // vaId,
vaEmail, // vaEmail,
projectId // projectId
}); // });
try { // try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = { // const sfDataSync = {
"_id": logoutLog._id, // "_id": logoutLog._id,
"loggedType": "LOGOUT", // "loggedType": "LOGOUT",
"vaId": vaId, // "vaId": vaId,
"vaEmail": vaEmail, // "vaEmail": vaEmail,
"projectId": projectId, // "projectId": projectId,
"time": logoutLog.time, // "time": logoutLog.time,
} // }
const response = await fetch(salesforceUrl, { // const response = await fetch(salesforceUrl, {
method: 'POST', // method: 'POST',
headers: { // headers: {
'Content-Type': 'application/json', // 'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication // // Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN // // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
}, // },
body: JSON.stringify(sfDataSync) // body: JSON.stringify(sfDataSync)
}); // });
if (!response.ok) { // if (!response.ok) {
const errorText = await response.text(); // const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
} // }
const result = await response.json(); // const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // // console.log("✅ Successfully synced VA data to Salesforce:", result);
return { // return {
shouldLogout: true, // shouldLogout: true,
totalTodayHours: totalTodayHours, // totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours, // dedicatedHours: project.dedicatedHours,
logoutLogId: logoutLog._id, // logoutLogId: logoutLog._id,
SyncSalesforce: result // SyncSalesforce: result
}; // };
} catch (error) { // } catch (error) {
console.error("Failed to sync with Salesforce:", error.message); // console.error("Failed to sync with Salesforce:", error.message);
console.log(error) // console.log(error)
// Create a custom error to distinguish Salesforce errors // // Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true; // salesforceError.isSalesforceError = true;
throw salesforceError; // throw salesforceError;
} // }
} // }
return { // return {
shouldLogout: false, // shouldLogout: false,
totalTodayHours: totalTodayHours, // totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours // dedicatedHours: project.dedicatedHours
}; // };
} catch (error) { // } catch (error) {
console.error("Error in checkDailyHoursAndLogout:", error); // console.error("Error in checkDailyHoursAndLogout:", error);
return { shouldLogout: false, error: error.message }; // return { shouldLogout: false, error: error.message };
} // }
} // }
// Getting logged hours in a specific project of VA // Getting logged hours in a specific project of VA
async function va_log_track_work_callback(req, res) { async function va_log_track_work_callback(req, res) {
try { try {
const accessToken = req.headers.token; // const accessToken = req.headers.token;
if (!accessToken) { // if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" }); // return res.status(401).json({ success: false, message: "Missing token" });
} // }
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); // const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) { // if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" }); // return res.status(401).json({ success: false, message: "Invalid token" });
} // }
const vaEmail = decoded.email; // const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); // const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) { // if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" }); // return res.status(404).json({ success: false, message: "VA not found" });
} // }
const vaId = vaUser._id; // const vaId = vaUser._id;
const payload = req.body || {}; const payload = req.body || {};
let { projectId, logId, title, appName, url = null } = payload; let { vaId, projectId, logId, logs } = payload;
if (title === "") { if (!projectId || !logId || !vaId || !Array.isArray(logs)) {
title = "" return res.status(400).json({ success: false, message: "Missing Props" });
} }
const vaUser = await VAModel.findById(vaId);
if (!vaUser) return res.status(400).json({ success: false, message: "VA not found" });
const project = await ProjectModel.findById(projectId); const project = await ProjectModel.findById(projectId);
if (!project) return res.status(400).json({ success: false, message: "Project not found" }); if (!project) return res.status(400).json({ success: false, message: "Project not found" });
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) {
return res.status(400).json({ success: false, message: "No logs to track" });
}
// Check last log entry for this VA and project // Check last log entry for this VA and project
const lastLog = await LoggedModel.findOne({ vaId, projectId }) const lastLog = await LoggedModel.findOne({ vaId, projectId })
.sort({ time: -1 }) // get the most recent log .sort({ time: -1 }) // get the most recent log
@ -173,104 +180,94 @@ async function va_log_track_work_callback(req, res) {
}); });
} }
if (!projectId || !logId || !title || !appName) {
return res.status(400).json({ success: false, message: "Missing Props" });
}
// 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) => {
const createdTrack = await WorkTrackModel.create({
vaId,
projectId,
logId,
titlew: log.title,
appName: log.appName,
url: log.url,
from: log.from,
to: log.to, // Fixed: Changed from duplicate "from" to "to"
totalTime: log.totalTime,
});
if (dailyCheck.shouldLogout) { return createdTrack;
return res.status(400).json({ })
success: false, );
message: `Cannot start work - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`,
autoLogout: true
});
}
const createdTrack = await WorkTrackModel.create({
vaId,
projectId,
logId,
title,
appName,
url,
from: Date.now(),
});
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: "Work tracking started", message: "Work tracking created",
data: createdTrack, data: createdTracks
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

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

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