This commit is contained in:
=bhagwanSingh 2025-11-20 12:54:28 +05:30
commit e1fc8d9d13
11 changed files with 514 additions and 70 deletions

View File

@ -71,7 +71,7 @@ async function va_login_client(req, res) {
}, },
}); });
} catch (error) { } catch (error) {
console.error("va_register error:", error); console.error("va_client_login_register 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,58 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, LoggedModel } from "../../../models/vaModel.js"; import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.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 }
]
});
// 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}`);
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id
};
} catch (error) {
console.error("Error closing previous session:", error);
throw error;
}
}
// Login VA // Login VA
async function va_login_callback(req, res) { async function va_login_callback(req, res) {
@ -32,34 +84,20 @@ async function va_login_callback(req, res) {
return res.status(401).json({ success: false, message: "Invalid password" }); return res.status(401).json({ success: false, message: "Invalid password" });
} }
// 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) // Check if VA has any active sessions (LOGIN without LOGOUT)
const activeSessions = await LoggedModel.find({ const activeSessions = await LoggedModel.find({
vaId: user._id, vaId: user._id
loggedType: "LOGIN"
}).sort({ time: -1 }); }).sort({ time: -1 });
let closeResult = [];
if (activeSessions.length > 0) { if (activeSessions.length > 0) {
// Get the latest login to check if there's no corresponding logout // Get the latest login to check if there's no corresponding logout
const latestLogin = activeSessions[0]; const latestLog = activeSessions[0];
closeResult = await closePreviousSession(user._id, latestLog);
// Check if there's a logout after this login
const correspondingLogout = await LoggedModel.findOne({
vaId: user._id,
loggedType: "LOGOUT",
time: { $gt: latestLogin.time }
});
if (!correspondingLogout) {
// VA is still logged in to a project
const projectInfo = await LoggedModel.findById(latestLogin._id)
.populate('projectId', 'name')
.lean();
return res.status(400).json({
success: false,
message: "You are still logged in to a project. Please logout first before logging in again."
});
}
} }
// Generate JWT accessToken with 24h expiry // Generate JWT accessToken with 24h expiry
@ -88,8 +126,10 @@ async function va_login_callback(req, res) {
$set: { accessToken, refreshToken } $set: { accessToken, refreshToken }
}) })
// Return accessToken and user info let responseData = [];
return res.status(200).json({
if (closeResult) {
responseData = {
success: true, success: true,
message: "Login Successfull", message: "Login Successfull",
accessToken, accessToken,
@ -99,7 +139,27 @@ async function va_login_callback(req, res) {
name: user.userDetails.firstName, name: user.userDetails.firstName,
email: user.userDetails.email, email: user.userDetails.email,
}, },
}); previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId
}
}
} else {
responseData = {
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
}
}
}
// Return accessToken and user info
return res.status(200).json(responseData);
} catch (error) { } catch (error) {
console.error("va_register error:", error); console.error("va_register 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,91 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day
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 };
}
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 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
});
return {
shouldLogout: true,
totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours,
logoutLogId: logoutLog._id
};
}
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 // 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 {
@ -50,6 +135,16 @@ async function va_log_track_work_callback(req, res) {
// Handle POST → create entry // Handle POST → create entry
if (req.method === "POST") { 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({ const createdTrack = await WorkTrackModel.create({
vaId, vaId,
projectId, projectId,
@ -63,6 +158,11 @@ async function va_log_track_work_callback(req, res) {
success: true, success: true,
message: "Work tracking started", message: "Work tracking started",
data: createdTrack, data: createdTrack,
dailyHours: {
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
remaining: formatHoursToReadable(dailyCheck.dedicatedHours - dailyCheck.totalTodayHours)
}
}); });
} }
@ -89,11 +189,14 @@ async function va_log_track_work_callback(req, res) {
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);
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();
return res.status(200).json({ const response = {
success: true, success: true,
message: "Work tracking stopped", message: "Work tracking stopped",
data: { data: {
@ -101,9 +204,24 @@ async function va_log_track_work_callback(req, res) {
from: workEntry.from, from: workEntry.from,
to: workEntry.to, to: workEntry.to,
formattedTotalTime, formattedTotalTime,
totalTimeMs 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;
}
return res.status(200).json(response);
} }
// Fallback for unsupported methods // Fallback for unsupported methods

View File

@ -1,4 +1,82 @@
import { LoggedModel } from "../../../models/vaModel.js"; import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day
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 };
}
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`;
}
}
// 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 }
]
});
// 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}`);
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id
};
} catch (error) {
console.error("Error closing previous session:", error);
throw error;
}
}
// Getting logged hours in a specific project of VA // Getting logged hours in a specific project of VA
async function va_project_log_callback(req, res) { async function va_project_log_callback(req, res) {
@ -7,9 +85,114 @@ async function va_project_log_callback(req, res) {
const { vaId, vaEmail, projectId, type } = payload; const { vaId, vaEmail, projectId, type } = payload;
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props"); if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); const project = await ProjectModel.findById(projectId);
if (!project) {
return res.status(400).json({ success: false, message: "Project not found" });
}
// For LOGIN requests, check daily hours
if (type === "LOGIN") {
const { start, end } = getDayRange();
// 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)})`
});
}
// 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);
// 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
});
return res.status(201).send({
success: true,
message: `Login Successful (previous session closed)`,
logId: createdLog._id,
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId
}
});
}
}
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
// console.log(createdLog)
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 });
} 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 res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) { } catch (error) {
return res.status(500).send("Internal Server Error"); return res.status(500).send("Internal Server Error");
} }

View File

@ -9,6 +9,21 @@ function formatHMS(ms) {
return `${h}h ${m}m ${s}s`; return `${h}h ${m}m ${s}s`;
} }
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`;
}
}
async function va_projects_log_list_function(req, res) { async function va_projects_log_list_function(req, res) {
try { try {
const { projectId, from, to } = req.params; const { projectId, from, to } = req.params;
@ -101,7 +116,9 @@ async function va_projects_log_list_function(req, res) {
activityStatus, activityStatus,
totalWorkTime: formatHMS(totalWorkMs), totalWorkTime: formatHMS(totalWorkMs),
totalWorkMs, totalWorkMs,
totalLogs: logs.length dedicatedHours: formatHoursToReadable(project?.dedicatedHours),
totalLogs: logs.length,
status: project?.status
}); });
} }

View File

@ -91,7 +91,7 @@ const projectSchema = new Schema({
}, },
], ],
dedicatedHours: { dedicatedHours: {
type: Number, type: String,
default: 0 default: 0
}, },
status: { status: {
@ -311,8 +311,8 @@ const vaWorkTrackSchema = new Schema({
ref: "Project", ref: "Project",
}, },
logId: { logId: {
type: String, type: Types.ObjectId,
ref: "Logged", ref: "TrackLog",
}, },
title: { title: {
type: String, type: String,

View File

@ -13,16 +13,32 @@ import cors from 'cors';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import apiRoutes from './routes/index.js' import apiRoutes from './routes/index.js'
import { render_reset_form } from "./controllers/passwordReset.controllers.js"; import { render_reset_form } from "./controllers/passwordReset.controllers.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
const app = express(); const app = express();
const server = new createServer(app); const server = new createServer(app);
const io = new Server(server); const io = new Server(server);
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("⚠️ Certificate error:", url);
// Allow for ngrok only
if (url.includes("ngrok")) {
event.preventDefault();
callback(true); // approve the certificate
} else {
callback(false); // reject others
}
});
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("⚠️ Certificate error:", url); console.log("⚠️ Certificate error:", url);
@ -70,10 +86,13 @@ app.get("/download-file", (req, res) => {
} }
}); });
app.get('/varegister/:sf_token?', (req, res) => { app.get('/varegister/:sf_token?', (req, res) => {
const { sf_token } = req.params; const { sf_token } = req.params;
try {
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
const { sf_token } = req.params;
try { try {
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN); const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
@ -95,6 +114,9 @@ app.get('/varegister/:sf_token?', (req, res) => {
app.get('/clientregister/:sf_token?', (req, res) => { app.get('/clientregister/:sf_token?', (req, res) => {
// portalId: RECORDid => 8990928342309
// email
// const { sf_token } = useParams(); // const { sf_token } = useParams();
res.render('va_client_register', { title: "Client Registration" }); res.render('va_client_register', { title: "Client Registration" });
@ -166,6 +188,27 @@ app.get('/client/virtual-assistants', 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" });
}
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" });
res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
app.get("/live/:projectId/:vaId", async (req, res) => { app.get("/live/:projectId/:vaId", async (req, res) => {
const { projectId, vaId } = req.params; const { projectId, vaId } = req.params;
@ -403,12 +446,34 @@ io.on("connection", (socket) => {
}); });
}); });
app.get("/client/login", (req, res) => {
res.render("client_login")
}) // io.on('connection', (socket) => {
app.get("/client/forgot-password", (req, res) => {
res.render("forgot_password") // socket.on("sendImage", (base64String) => {
}) // io.emit("receiveImage", base64String);
// });
// socket.on('disconnect', () => {
// console.log('a user Disconnected');
// });
// });
// Express App Listening
// app.listen(PORT, () => {
// mongoose
// .connect(
// process.env.MONGO_URI,
// )
// .then(() => console.log("mongodb connected"))
// .catch((err) => {
// console.log(err.message);
// process.exit(1);
// });
// console.log(`Express started on http://localhost:${PORT}`);
// });
app.use((req, res) => { app.use((req, res) => {
res.status(404).render("not_found", { res.status(404).render("not_found", {

View File

@ -43,8 +43,8 @@ class SalesforceService {
const { userDetails } = clientData; const { userDetails } = clientData;
return { return {
_id: _id,
userDetails: { userDetails: {
_id: _id,
name: userDetails.name, name: userDetails.name,
email: userDetails.email.toLowerCase(), email: userDetails.email.toLowerCase(),
address: userDetails.address || null, address: userDetails.address || null,

View File

@ -17,11 +17,13 @@ class VaClientService {
// Sanitize and prepare data // Sanitize and prepare data
const clientData = this.sanitizeClientData(data); const clientData = this.sanitizeClientData(data);
// Send data to Salesforce first
await this.salesforceService.registerClientInSalesforce(data, clientData._id);
const response = await this.clientRepository.create(clientData); const response = await this.clientRepository.create(clientData);
if(response) {
await this.salesforceService.registerClientInSalesforce(data, response._id);
}
// Send data to Salesforce first
return response; return response;
} catch (error) { } catch (error) {
logger.error("Client Service - registerClient error: " + error.message); logger.error("Client Service - registerClient error: " + error.message);

View File

@ -277,10 +277,10 @@
throw new Error(data.message || "Login failed"); throw new Error(data.message || "Login failed");
} }
if (data.accessToken) { if (data.success) {
// 🟢 Save token // 🟢 Save token
localStorage.setItem("knoptpq", data.accessToken); localStorage.setItem("knoptpq", data.accessToken);
localStorage.setItem("refreshToken", data.refreshToken); // localStorage.setItem("refreshToken", data.refreshToken);
// 🟢 Redirect // 🟢 Redirect
window.location.href = "/client/dashboard"; window.location.href = "/client/dashboard";

View File

@ -163,7 +163,6 @@
</head> </head>
<body> <body>
<!-- Mobile Toggle Button --> <!-- Mobile Toggle Button -->
<div class="mobile-toggle" id="toggleBtn"> <div class="mobile-toggle" id="toggleBtn">
<span></span> <span></span>