diff --git a/src/controllers/va/vaAuth/vaLogout.controller.js b/src/controllers/va/vaAuth/vaLogout.controller.js index 8df33b9..602417c 100644 --- a/src/controllers/va/vaAuth/vaLogout.controller.js +++ b/src/controllers/va/vaAuth/vaLogout.controller.js @@ -13,7 +13,7 @@ async function va_logout_callback(req, res) { return res.status(404).json({ success: false, message: "Invalid credentials" }); } - await VAModel.updateOne({ "userDetails.email": userDetails.email }, { + await VAModel.updateOne({ "userDetails.email": vaEmail }, { $set: { accessToken: null, refreshToken: null } }) diff --git a/src/controllers/va/vaLog/vaListLog.controller.js b/src/controllers/va/vaLog/vaListLog.controller.js index 2b60b70..770952d 100644 --- a/src/controllers/va/vaLog/vaListLog.controller.js +++ b/src/controllers/va/vaLog/vaListLog.controller.js @@ -1,4 +1,4 @@ -import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js"; +import { VAModel, LoggedModel } from "../../../models/vaModel.js"; // Getting all active VA's and their details async function va_callback(req, res) { diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js new file mode 100644 index 0000000..e1d908e --- /dev/null +++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js @@ -0,0 +1,117 @@ +import jwt from "jsonwebtoken"; +import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; + +// Getting logged hours in a specific project of VA +async function va_log_track_work_callback(req, res) { + try { + const accessToken = req.headers.token; + + if (!accessToken) { + 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 || {}; + const { projectId, logId, title, appName } = payload; + + const project = await ProjectModel.findById(projectId); + if (!project) return res.status(400).json({ success: false, message: "Project not found" }); + + const logEntry = await LoggedModel.findById(logId); + if (!logEntry) return res.status(400).json({ success: false, message: "Log entry not found" }); + + // 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", + }); + } + + if (!projectId || !logId || !title || !appName) { + return res.status(400).json({ success: false, message: "Missing Props" }); + } + + // Handle POST → create entry + if (req.method === "POST") { + const createdTrack = await WorkTrackModel.create({ + vaId, + projectId, + logId, + title, + appName, + from: Date.now(), + }); + + return res.status(201).json({ + success: true, + message: "Work tracking started", + data: createdTrack, + }); + } + + // Handle PATCH → update entry (add 'to' and calculate total time) + if (req.method === "PATCH") { + const { id } = payload; // expecting the workTrack record ID to update + if (!id) { + return res.status(400).json({ success: false, message: "Missing workTrack ID" }); + } + + const workEntry = await WorkTrackModel.findById(id); + if (!workEntry) { + return res.status(404).json({ success: false, message: "Work entry not found" }); + } + + const toTime = new Date(); + const fromTime = new Date(workEntry.from); + const totalTimeMs = toTime - fromTime; + + // Optional: store totalTime in minutes or hours instead of raw ms + const hours = Math.floor(totalTimeMs / (1000 * 60 * 60)); + const minutes = Math.floor((totalTimeMs % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((totalTimeMs % (1000 * 60)) / 1000); + + const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`; + + workEntry.to = toTime; + workEntry.totalTime = totalTimeMs; // store as number of minutes + await workEntry.save(); + + return res.status(200).json({ + success: true, + message: "Work tracking stopped", + data: { + id: workEntry._id, + from: workEntry.from, + to: workEntry.to, + formattedTotalTime, + totalTimeMs + }, + }); + } + + // Fallback for unsupported methods + return res.status(405).json({ success: false, message: "Method not allowed" }); + } catch (error) { + return res.status(500).send("Internal Server Error"); + } +} + +export { va_log_track_work_callback }; \ No newline at end of file diff --git a/src/controllers/va/vaLog/vaLogWorkReport.js b/src/controllers/va/vaLog/vaLogWorkReport.js new file mode 100644 index 0000000..fd9344f --- /dev/null +++ b/src/controllers/va/vaLog/vaLogWorkReport.js @@ -0,0 +1,116 @@ +import jwt from "jsonwebtoken"; +import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; + +// Getting logged hours in a specific project of VA +async function va_log_work_report_callback(req, res) { + try { + const accessToken = req.headers.token; + + if (!accessToken) { + 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 || {}; + const { fromDate, toDate } = payload; + + const start = fromDate ? new Date(fromDate) : new Date(); + const end = toDate ? new Date(toDate) : new Date(); + + if (!fromDate && !toDate) { + start.setHours(0, 0, 0, 0); + end.setHours(23, 59, 59, 999); + } + + const logs = await LoggedModel.find({ + vaId, + time: { $gte: start, $lte: end }, + }) + .sort({ time: 1 }) + .populate("projectId", "name") // attach project name + .lean(); + + if (!logs.length) { + return res.status(200).json({ + success: true, + message: "No logs found in given range", + data: [], + totalWorkTime: "00:00:00", + }); + } + + let totalWorkSeconds = 0; + const projectWork = {}; // key: projectId -> seconds + + let lastLogin = null; + let lastProjectId = null; + + // ⏱️ Calculate work duration + for (const log of logs) { + const time = new Date(log.time); + + if (log.loggedType === "LOGIN") { + lastLogin = time; + lastProjectId = log.projectId?._id?.toString() || null; + } else if (log.loggedType === "LOGOUT" && lastLogin) { + const diffSec = (time - lastLogin) / 1000; + totalWorkSeconds += diffSec; + + if (lastProjectId) { + projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec; + } + + // reset after each pair + lastLogin = null; + lastProjectId = null; + } + } + + // 🕓 Convert total seconds to hh:mm:ss + const formatTime = (seconds) => { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + }; + + // 🧠 Build project-wise array + const projectDetails = await Promise.all( + Object.entries(projectWork).map(async ([projectId, sec]) => { + const project = await ProjectModel.findById(projectId).lean(); + return { + projectId, + projectName: project?.name || "Unknown Project", + totalTime: formatTime(sec), + }; + }) + ); + + return res.status(200).json({ + success: true, + from: start, + to: end, + totalWorkTime: formatTime(totalWorkSeconds), + projects: projectDetails, + logs, // optional raw logs for debugging + }); + + } catch (error) { + return res.status(500).send("Internal Server Error"); + } +} + +export { va_log_work_report_callback }; diff --git a/src/controllers/va/vaProfile/vaProfile.controller.js b/src/controllers/va/vaProfile/vaProfile.controller.js new file mode 100644 index 0000000..869de05 --- /dev/null +++ b/src/controllers/va/vaProfile/vaProfile.controller.js @@ -0,0 +1,44 @@ +import jwt from "jsonwebtoken"; +import { VAModel } from "../../../models/vaModel.js"; + +// Register a new VA +async function va_profile_callback(req, res) { + try { + const accessToken = req.headers.token; + + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing access token" }); + } + + let decoded; + try { + decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + } catch (err) { + return res.status(401).json({ success: false, message: "Invalid or expired token" }); + } + + if (!decoded?.email) { + return res.status(400).json({ success: false, message: "Invalid token payload" }); + } + + const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }) + .select("-userDetails.password") // hide password + .populate("clientIds", "userDetails.email userDetails.name") // optional populate + .populate("projectIds", "name totalLoggedHours") // optional populate + .lean(); + + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } + + return res.status(200).json({ + success: true, + message: "VA profile", + data: vaUser + }); + } catch (error) { + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_profile_callback }; \ No newline at end of file diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js new file mode 100644 index 0000000..d00b0b3 --- /dev/null +++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js @@ -0,0 +1,86 @@ +import jwt from "jsonwebtoken"; +import bcrypt from "bcrypt"; +import { VAModel } from "../../../models/vaModel.js"; + +// Register a new VA +async function va_profile_update_callback(req, res) { + try { + const accessToken = req.headers.token; + + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing access token" }); + } + + let decoded; + try { + decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + } catch (err) { + return res.status(401).json({ success: false, message: "Invalid or expired token" }); + } + + if (!decoded?.email) { + return res.status(400).json({ success: false, message: "Invalid token payload" }); + } + + const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }); + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } + + const payload = req.body || {}; + const { userDetails } = payload; + + if (!userDetails) { + return res.status(400).json({ success: false, message: "Missing userDetails" }); + } + + // Allowed fields to update + const allowedFields = [ + "firstName", "lastName", "dob", "age", "address", + "bankName", "accountHolderName", "accountNumber", "routingNumber" + ]; + + // Update allowed fields only + allowedFields.forEach(field => { + if (userDetails[field] !== undefined) { + vaUser.userDetails[field] = userDetails[field]; + } + }); + + if (userDetails.password) { + if (!userDetails.cPassword) { + return res.status(400).json({ + success: false, + message: "Please confirm your new password", + }); + } + + if (userDetails.password !== userDetails.cPassword) { + return res.status(400).json({ + success: false, + message: "Passwords do not match", + }); + } + + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds); + vaUser.userDetails.password = hashedPassword; + } + + await vaUser.save(); + + const updatedUser = await VAModel.findById(vaUser._id) + .select("-userDetails.password") + .lean(); + + return res.status(200).json({ + success: true, + message: "Profile updated successfully", + data: updatedUser, + }); + } catch (error) { + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_profile_update_callback }; \ No newline at end of file diff --git a/src/controllers/va/vaAuth/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js similarity index 100% rename from src/controllers/va/vaAuth/vaRegister.controller.js rename to src/controllers/va/vaProfile/vaRegister.controller.js diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js index f4d79f0..ba34201 100644 --- a/src/controllers/va/vaProjects/vaProjectLog.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js @@ -7,9 +7,9 @@ async function va_project_log_callback(req, res) { const { vaId, vaEmail, projectId, type } = payload; if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props"); - await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId }) + const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId }) - return res.status(201).send({ success: true, message: `${type} Successfully` }); + 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/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 815f65e..0f1ea53 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -1,5 +1,6 @@ import jwt from "jsonwebtoken"; import { VAModel } from "../models/vaModel.js"; +import logger from "../config/logger.js"; async function authValidator(req, res, next) { try { diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 1cab614..15e9105 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -14,6 +14,7 @@ const userDetailsSchema = new Schema({ email: { type: String, required: true, + unique: true, }, dob: { type: String, @@ -167,30 +168,50 @@ const vaSchema = new Schema( // Need to ask to client if we want to provide schedule changing functionality on Docker toooooo -// Add IN OUT +// Add working on Tabs Datils - like which tabs VA is working on while working on project - Tab Model +// Add activity level of VA while working on project - Activity Model -// const vaTimeSheetSchema = new Schema({ -// vaId: { -// type: Types.ObjectId, -// ref: "VA", -// }, -// projectId: { -// type: Types.ObjectId, -// ref: "Project", -// }, -// logged: { -// enum: ["IN", "OUT"], -// default: "IN", -// } -// }, -// { timestamps: true } -// ) +const vaWorkTrackSchema = new Schema({ + vaId: { + type: Types.ObjectId, + ref: "VA", + }, + projectId: { + type: Types.ObjectId, + ref: "Project", + }, + logId: { + type: String, + ref: "Logged", + }, + title: { + type: String, + }, + appName: { + type: String, + }, + from: { + type: Date, + default: Date.now, + }, + to: { + type: Date, + default: Date.now, + }, + totalTime: { + type: Number, + default: 0, + }, +}, + { timestamps: true } +) /* ---------------------- Model Exports ---------------------- */ const VAModel = model("VA", vaSchema); const ClientModel = model("Client", clientSchema); const ProjectModel = model("Project", projectSchema); const LoggedModel = model("TrackLog", loggedSchema); +const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel }; +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel }; diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 86f9e3b..96959a3 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -2,7 +2,9 @@ import express from "express"; const vaRoutes = express.Router(); import { va_username_callback } from "../../controllers/va/vaUsername.controller.js"; -import { va_register } from "../../controllers/va/vaAuth/vaRegister.controller.js"; +import { va_register } from "../../controllers/va/vaProfile/vaRegister.controller.js"; +import { va_profile_callback } from "../../controllers/va/vaProfile/vaProfile.controller.js"; +import { va_profile_update_callback } from "../../controllers/va/vaProfile/vaProfileUpdate.controller.js"; import { va_login_callback } from "../../controllers/va/vaAuth/vaLogin.controller.js"; import { va_refresh_token_callback } from "../../controllers/va/vaAuth/vaRefreshToken.controller.js"; import { va_logout_callback } from "../../controllers/va/vaAuth/vaLogout.controller.js"; @@ -14,35 +16,39 @@ import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaC import { va_project_log_callback } from "../../controllers/va/vaProjects/vaProjectLog.controller.js"; import { va_projects_callback } from "../../controllers/va/vaProjects/vaProjects.controller.js"; import { authValidator } from "../../middlewares/auth.middleware.js"; +import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrackWork.controller.js"; +import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js"; vaRoutes.get("/", va_callback); // VA Callback for getting active 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.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token - vaRoutes.post("/register", va_register); // Register a new VA +vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile +vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile + vaRoutes.get("/:userName", va_username_callback); // VA Username Callback - -vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID - -vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback - -vaRoutes.get("/project/:projectId", va_projects_callback); // Projects Callback for getting project details - -// FROM || TO - -vaRoutes.get("/log/:projectId/:from?/:to?", authValidator, va_log_callback); // VA Log Callback - -vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged Hours in specific Project - -// VA filter search: supports query params `name`, `email`, `from`, `to` // Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 vaRoutes.get("/search", va_filter_search); +vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID +vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback + +vaRoutes.get("/project/:projectId", va_projects_callback); + + +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.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/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report +// VA filter search: supports query params `name`, `email`, `from`, `to` + +