diff --git a/src/controllers/client/vaClientLogin.controller.js b/src/controllers/client/vaClientLogin.controller.js index 384c132..466fcea 100644 --- a/src/controllers/client/vaClientLogin.controller.js +++ b/src/controllers/client/vaClientLogin.controller.js @@ -71,7 +71,7 @@ async function va_login_client(req, res) { }, }); } 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" }); } } diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index 580552d..08a173b 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -1,6 +1,58 @@ import bcrypt from "bcrypt"; 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 async function va_login_callback(req, res) { @@ -32,36 +84,22 @@ async function va_login_callback(req, res) { 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) const activeSessions = await LoggedModel.find({ - vaId: user._id, - loggedType: "LOGIN" + vaId: user._id }).sort({ time: -1 }); - + + let closeResult = []; + if (activeSessions.length > 0) { // 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 const accessToken = jwt.sign( { @@ -88,18 +126,40 @@ async function va_login_callback(req, res) { $set: { accessToken, refreshToken } }) + let responseData = []; + + if (closeResult) { + responseData = { + success: true, + message: "Login Successfull", + accessToken, + refreshToken, + user: { + id: user._id, + name: user.userDetails.firstName, + email: user.userDetails.email, + }, + previousSession: { + closedWorkTracks: closeResult.closedWorkTracks, + logoutLogId: closeResult.logoutLogId + } + } + } else { + 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({ - success: true, - message: "Login Successfull", - accessToken, - refreshToken, - user: { - id: user._id, - name: user.userDetails.firstName, - email: user.userDetails.email, - }, - }); + return res.status(200).json(responseData); } catch (error) { console.error("va_register error:", error); return res.status(500).json({ success: false, message: "Internal Server Error" }); diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js index 97bd382..89366f6 100644 --- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js +++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js @@ -1,6 +1,91 @@ import jwt from "jsonwebtoken"; 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 async function va_log_track_work_callback(req, res) { try { @@ -50,6 +135,16 @@ async function va_log_track_work_callback(req, res) { // Handle POST → create entry if (req.method === "POST") { + const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project); + + if (dailyCheck.shouldLogout) { + return res.status(400).json({ + success: false, + message: `Cannot start work - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`, + autoLogout: true + }); + } + const createdTrack = await WorkTrackModel.create({ vaId, projectId, @@ -63,6 +158,11 @@ async function va_log_track_work_callback(req, res) { success: true, message: "Work tracking started", 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`; + // Check if daily dedicated hours are completed after this work session + const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs); + workEntry.to = toTime; workEntry.totalTime = totalTimeMs; // store as number of minutes await workEntry.save(); - return res.status(200).json({ + const response = { success: true, message: "Work tracking stopped", data: { @@ -101,9 +204,24 @@ async function va_log_track_work_callback(req, res) { from: workEntry.from, to: workEntry.to, 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 diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js index ba34201..a06daaa 100644 --- a/src/controllers/va/vaProjects/vaProjectLog.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js @@ -1,15 +1,198 @@ -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 async function va_project_log_callback(req, res) { try { const payload = req.body || {}; const { vaId, vaEmail, projectId, type } = payload; - + 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) { return res.status(500).send("Internal Server Error"); } diff --git a/src/controllers/va/vaProjects/vaProjectLogList.controller.js b/src/controllers/va/vaProjects/vaProjectLogList.controller.js index bd7e541..13bafb5 100644 --- a/src/controllers/va/vaProjects/vaProjectLogList.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLogList.controller.js @@ -9,6 +9,21 @@ function formatHMS(ms) { 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) { try { const { projectId, from, to } = req.params; @@ -101,7 +116,9 @@ async function va_projects_log_list_function(req, res) { activityStatus, totalWorkTime: formatHMS(totalWorkMs), totalWorkMs, - totalLogs: logs.length + dedicatedHours: formatHoursToReadable(project?.dedicatedHours), + totalLogs: logs.length, + status: project?.status }); } diff --git a/src/models/vaModel.js b/src/models/vaModel.js index bba648c..f05cd95 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -91,7 +91,7 @@ const projectSchema = new Schema({ }, ], dedicatedHours: { - type: Number, + type: String, default: 0 }, status: { @@ -311,8 +311,8 @@ const vaWorkTrackSchema = new Schema({ ref: "Project", }, logId: { - type: String, - ref: "Logged", + type: Types.ObjectId, + ref: "TrackLog", }, title: { type: String, diff --git a/src/server.js b/src/server.js index 587e7f0..b62f713 100644 --- a/src/server.js +++ b/src/server.js @@ -13,16 +13,32 @@ import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; + import apiRoutes from './routes/index.js' + import { render_reset_form } from "./controllers/passwordReset.controllers.js"; import jwt from "jsonwebtoken"; const app = express(); + const server = new createServer(app); 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) => { console.log("⚠️ Certificate error:", url); @@ -70,10 +86,13 @@ app.get("/download-file", (req, res) => { } }); - app.get('/varegister/:sf_token?', (req, res) => { const { sf_token } = req.params; + try { + const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN); + const { sf_token } = req.params; + try { 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) => { + // portalId: RECORDid => 8990928342309 + // email + // const { sf_token } = useParams(); 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) => { const { projectId, vaId } = req.params; @@ -403,12 +446,34 @@ io.on("connection", (socket) => { }); }); -app.get("/client/login", (req, res) => { - res.render("client_login") -}) -app.get("/client/forgot-password", (req, res) => { - res.render("forgot_password") -}) + + +// io.on('connection', (socket) => { + +// 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) => { res.status(404).render("not_found", { diff --git a/src/services/salesforce.service.js b/src/services/salesforce.service.js index 3f310ad..f82ed1b 100644 --- a/src/services/salesforce.service.js +++ b/src/services/salesforce.service.js @@ -43,8 +43,8 @@ class SalesforceService { const { userDetails } = clientData; return { - _id: _id, userDetails: { + _id: _id, name: userDetails.name, email: userDetails.email.toLowerCase(), address: userDetails.address || null, diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js index 0feaa4c..b6c971b 100644 --- a/src/services/vaClient.service.js +++ b/src/services/vaClient.service.js @@ -17,11 +17,13 @@ class VaClientService { // Sanitize and prepare 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); + + if(response) { + await this.salesforceService.registerClientInSalesforce(data, response._id); + } + // Send data to Salesforce first + return response; } catch (error) { logger.error("Client Service - registerClient error: " + error.message); diff --git a/views/client_login.ejs b/views/client_login.ejs index 0f68c3e..4b9e36f 100644 --- a/views/client_login.ejs +++ b/views/client_login.ejs @@ -276,11 +276,11 @@ if (!res.ok) { throw new Error(data.message || "Login failed"); } - - if (data.accessToken) { + + if (data.success) { // 🟢 Save token localStorage.setItem("knoptpq", data.accessToken); - localStorage.setItem("refreshToken", data.refreshToken); + // localStorage.setItem("refreshToken", data.refreshToken); // 🟢 Redirect window.location.href = "/client/dashboard"; diff --git a/views/va_live_tracker.ejs b/views/va_live_tracker.ejs index 83dbf23..192432a 100644 --- a/views/va_live_tracker.ejs +++ b/views/va_live_tracker.ejs @@ -3,13 +3,13 @@
-