456 lines
18 KiB
JavaScript
456 lines
18 KiB
JavaScript
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.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 };
|
|
}
|
|
|
|
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) {
|
|
return `${wholeHours}h`;
|
|
} else {
|
|
return `${wholeMinutes}m`;
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
parsedDate.setMilliseconds(0);
|
|
|
|
console.log("pasdes date" + parsedDate)
|
|
return parsedDate;
|
|
} catch (error) {
|
|
console.error('Error parsing ISO time string:', error);
|
|
return new Date();
|
|
}
|
|
}
|
|
|
|
// Helper function to check if time difference is less than threshold
|
|
function isTimeWithinThreshold(receivedTime, currentTime, thresholdSeconds = 10) {
|
|
const diffMs = Math.abs(currentTime - receivedTime);
|
|
const diffSeconds = diffMs / 1000;
|
|
console.log(`Time comparison - Received: ${receivedTime.toISOString()}, Current: ${currentTime.toISOString()}, Diff: ${diffSeconds}s`);
|
|
return diffSeconds <= thresholdSeconds;
|
|
}
|
|
|
|
// Helper function to get current UTC time
|
|
function getCurrentUTCTime() {
|
|
const now = new Date();
|
|
now.setMilliseconds(0);
|
|
return now;
|
|
}
|
|
|
|
// function parseTimeToMinutes(timeString) {
|
|
// if (!timeString) return null;
|
|
|
|
// const [hours, minutes] = timeString.split(':').map(Number);
|
|
// return hours * 60 + (minutes || 0);
|
|
// }
|
|
|
|
// function parseDelayToMinutes(delayString) {
|
|
// if (!delayString) return 0;
|
|
|
|
// if (delayString.includes('h')) {
|
|
// const hours = parseInt(delayString.replace('h', '').trim());
|
|
// return hours * 60;
|
|
// } else if (delayString.includes('m')) {
|
|
// return parseInt(delayString.replace('m', '').trim());
|
|
// }
|
|
|
|
// return parseInt(delayString) || 0; // Default to minutes if no unit specified
|
|
// }
|
|
|
|
// function checkScheduleCompliance(currentTime, project, vaStatus, type) {
|
|
// const warnings = [];
|
|
|
|
// // If project doesn't have schedule or VA is PARTTIME with flexible allowed, no schedule restrictions
|
|
// if (!project.vaWorkingScheduleStart || !project.vaWorkingScheduleEnd) {
|
|
// return { isCompliant: true, warnings };
|
|
// }
|
|
|
|
// const currentDate = new Date(currentTime);
|
|
// const currentMinutes = currentDate.getUTCHours() * 60 + currentDate.getUTCMinutes();
|
|
|
|
// const scheduleStartMinutes = parseTimeToMinutes(project.vaWorkingScheduleStart);
|
|
// const scheduleEndMinutes = parseTimeToMinutes(project.vaWorkingScheduleEnd);
|
|
// const allowedStartDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedStart);
|
|
// const allowedEndDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedEnd);
|
|
|
|
// // Calculate allowed time ranges
|
|
// const allowedStartMinutes = scheduleStartMinutes - allowedStartDelay;
|
|
// const allowedEndMinutes = scheduleEndMinutes + allowedEndDelay;
|
|
|
|
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
|
|
|
|
// // Check for PARTTIME with flexible schedule
|
|
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) {
|
|
// 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 };
|
|
// }
|
|
|
|
// function to close open work tracks and create logout
|
|
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,
|
|
logId: lastLog._id,
|
|
$or: [
|
|
{ to: { $exists: false } },
|
|
{ totalTime: 0 },
|
|
{ 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",
|
|
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,
|
|
}
|
|
|
|
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 {
|
|
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;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("Error closing previous session:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Getting logged hours in a specific project of VA
|
|
async function va_project_log_callback(req, res) {
|
|
try {
|
|
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");
|
|
}
|
|
|
|
// 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;
|
|
console.log(`✅ Using received ISO time: ${receivedTimeString}, difference is within 10 seconds`);
|
|
console.log(`✅ Parsed as: ${receivedTime.toISOString()}`);
|
|
} else {
|
|
logTime = currentTime;
|
|
console.log(`⚠️ Using current UTC time, received time difference exceeds 10 seconds.`);
|
|
console.log(`Received: ${receivedTimeString} (parsed: ${receivedTime.toISOString()})`);
|
|
console.log(`Current: ${currentTime.toISOString()}`);
|
|
}
|
|
}
|
|
|
|
const project = await ProjectModel.findById(projectId);
|
|
if (!project) {
|
|
return res.status(400).json({ success: false, message: "Project not found" });
|
|
}
|
|
|
|
const vaUser = await VAModel.findById(vaId);
|
|
if (!vaUser) {
|
|
return res.status(400).json({ success: false, message: "VA not found" });
|
|
}
|
|
|
|
// Check schedule compliance
|
|
// const scheduleCheck = checkScheduleCompliance(logTime, project, vaUser.status, type);
|
|
// console.log(`Schedule compliance check:`, scheduleCheck);
|
|
// if (!scheduleCheck.isCompliant) {
|
|
// return res.status(400).json({
|
|
// success: false,
|
|
// message: `Cannot ${type.toLowerCase()} - outside of allowed schedule time`,
|
|
// warnings: scheduleCheck.warnings
|
|
// });
|
|
// }
|
|
|
|
// For LOGIN requests, check daily hours (using UTC day range)
|
|
if (type === "LOGIN") {
|
|
const { start, end } = getDayRange(); // UTC day range
|
|
// Get today's total hours
|
|
const todayWorkEntries = await WorkTrackModel.find({
|
|
vaId,
|
|
projectId,
|
|
from: { $gte: start, $lte: end }
|
|
});
|
|
|
|
const totalTodayMs = todayWorkEntries.reduce((total, entry) => {
|
|
return total + (entry.totalTime || 0);
|
|
}, 0);
|
|
|
|
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
|
|
|
|
// Check if daily hours are already completed
|
|
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)})`,
|
|
// warnings: scheduleCheck.warnings
|
|
});
|
|
}
|
|
|
|
// Check last log entry
|
|
const lastLog = await LoggedModel.findOne({ vaId })
|
|
.sort({ time: -1 })
|
|
.lean();
|
|
|
|
// Prevent login if last action was login (not logout)
|
|
if (lastLog && lastLog.loggedType === "LOGIN") {
|
|
|
|
// check last workModel login time and make a logout request then create login
|
|
// console.log(`User already logged in. Closing previous session...`);
|
|
const closeResult = await closePreviousSession(vaId, lastLog, logTime);
|
|
|
|
// 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,
|
|
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": "LOGIN",
|
|
"vaId": vaId,
|
|
"vaEmail": vaEmail,
|
|
"projectId": projectId,
|
|
"time": createdLog.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(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();
|
|
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: `${type} Successfully`,
|
|
logId: createdLog._id,
|
|
SyncSalesforce: result,
|
|
previousSession: {
|
|
closedWorkTracks: closeResult.closedWorkTracks,
|
|
logoutLogId: closeResult.logoutLogId
|
|
},
|
|
timeUsed: receivedTimeString ? 'received' : 'current',
|
|
actualTime: createdLog.time,
|
|
// warnings: scheduleCheck.warnings
|
|
});
|
|
} 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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(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();
|
|
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: `${type} Successfully`,
|
|
logId: createdLog._id,
|
|
SyncSalesforce: result,
|
|
timeUsed: receivedTimeString ? 'received' : 'current',
|
|
actualTime: createdLog.time,
|
|
// warnings: scheduleCheck.warnings
|
|
});
|
|
} 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 in va_project_log_callback:", error);
|
|
return res.status(500).send("Internal Server Error");
|
|
}
|
|
}
|
|
|
|
export { va_project_log_callback }; |