Added workreport, window tabs tracking api

This commit is contained in:
Aakash Mittal 2025-11-07 19:13:42 +05:30
commit e55183a2ef
11 changed files with 430 additions and 39 deletions

View File

@ -13,7 +13,7 @@ async function va_logout_callback(req, res) {
return res.status(404).json({ success: false, message: "Invalid credentials" }); 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 } $set: { accessToken: null, refreshToken: null }
}) })

View File

@ -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 // Getting all active VA's and their details
async function va_callback(req, res) { async function va_callback(req, res) {

View File

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

View File

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

View File

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

View File

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

View File

@ -7,9 +7,9 @@ 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");
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) { } catch (error) {
return res.status(500).send("Internal Server Error"); return res.status(500).send("Internal Server Error");
} }

View File

@ -1,5 +1,6 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel } from "../models/vaModel.js"; import { VAModel } from "../models/vaModel.js";
import logger from "../config/logger.js";
async function authValidator(req, res, next) { async function authValidator(req, res, next) {
try { try {

View File

@ -14,6 +14,7 @@ const userDetailsSchema = new Schema({
email: { email: {
type: String, type: String,
required: true, required: true,
unique: true,
}, },
dob: { dob: {
type: String, 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 // 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({ const vaWorkTrackSchema = new Schema({
// vaId: { vaId: {
// type: Types.ObjectId, type: Types.ObjectId,
// ref: "VA", ref: "VA",
// }, },
// projectId: { projectId: {
// type: Types.ObjectId, type: Types.ObjectId,
// ref: "Project", ref: "Project",
// }, },
// logged: { logId: {
// enum: ["IN", "OUT"], type: String,
// default: "IN", ref: "Logged",
// } },
// }, title: {
// { timestamps: true } 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 ---------------------- */ /* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema); const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema); const ClientModel = model("Client", clientSchema);
const ProjectModel = model("Project", projectSchema); const ProjectModel = model("Project", projectSchema);
const LoggedModel = model("TrackLog", loggedSchema); const LoggedModel = model("TrackLog", loggedSchema);
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel }; export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel };

View File

@ -2,7 +2,9 @@ import express from "express";
const vaRoutes = express.Router(); const vaRoutes = express.Router();
import { va_username_callback } from "../../controllers/va/vaUsername.controller.js"; 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_login_callback } from "../../controllers/va/vaAuth/vaLogin.controller.js";
import { va_refresh_token_callback } from "../../controllers/va/vaAuth/vaRefreshToken.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"; 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_project_log_callback } from "../../controllers/va/vaProjects/vaProjectLog.controller.js";
import { va_projects_callback } from "../../controllers/va/vaProjects/vaProjects.controller.js"; import { va_projects_callback } from "../../controllers/va/vaProjects/vaProjects.controller.js";
import { authValidator } from "../../middlewares/auth.middleware.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.get("/", va_callback); // VA Callback for getting active VA
vaRoutes.post("/login", va_login_callback); // VA Callback for login 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.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("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA 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("/: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 // 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("/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`