va-tracker-middleware/src/controllers/va.controller.js

616 lines
21 KiB
JavaScript

import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../models/vaModel.js";
async function logVaTime(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");
await LoggedModel.insert({ loggedType: type, vaId, vaEmail, projectId })
return res.status(201).send({ success: true, message: `${type} Successfully` });
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
// Getting all active VA's and their details
async function va_callback(req, res) {
try {
// Find the latest log entry for each VA and return those whose last entry is LOGIN
const agg = await LoggedModel.aggregate([
{ $sort: { time: -1 } },
{ $group: { _id: "$vaId", lastLog: { $first: "$$ROOT" } } },
{ $match: { "lastLog.loggedType": "LOGIN" } }
]);
const vaIds = agg.map((a) => a._id).filter(Boolean);
if (vaIds.length === 0) {
return res.status(200).json({ success: true, count: 0, data: [] });
}
const vas = await VAModel.find({ _id: { $in: vaIds } }).lean().exec();
// map lastLog by VA id for easy attachment
const lastLogByVa = agg.reduce((acc, a) => {
acc[String(a._id)] = a.lastLog;
return acc;
}, {});
const results = vas.map((v) => ({ ...v, lastLog: lastLogByVa[String(v._id)] || null }));
return res.status(200).json({ success: true, count: results.length, data: results });
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
// Register a new VA
async function va_register(req, res) {
try {
const payload = req.body ? JSON.parse(req.body) : {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) {
return res.status(400).json({ success: false, message: "Missing required fields" });
}
if (userDetails.password !== userDetails.cPassword) {
return res.status(400).json({ success: false, message: "password not matching" });
}
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
// sanitize/construct allowed fields
const toCreate = {
userDetails: {
firstName: userDetails.firstName,
lastName: userDetails.lastName || "",
email: userDetails.email,
dob: userDetails.dob || null,
age: userDetails.age || null,
address: userDetails.address || null,
password: hashedPassword,
bankName: userDetails.bankName || null,
accountHolderName: userDetails.accountHolderName || null,
accountNumber: userDetails.accountNumber || null,
routingNumber: userDetails.routingNumber || null,
},
tracking: payload.tracking || undefined,
clientIds: payload.clientIds || [],
schedule: payload.schedule || undefined,
status: payload.status || "FULLTIME",
};
const created = await VAModel.create(toCreate);
return res.status(201).json({ success: true, data: created });
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Login VA
async function va_login_callback(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.password) {
return res.status(400).json({ success: false, message: "Please provide email and password" });
}
// Find user by email
const user = await VAModel.findOne({ "userDetails.email": userDetails.email });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" });
}
// const saltRounds = 10;
// const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
// Compare password with stored hash
const isMatch = await bcrypt.compare(
userDetails.password,
user.userDetails.password
);
if (!isMatch) {
return res.status(401).json({ success: false, message: "Invalid password" });
}
// Generate JWT accessToken with 24h expiry
const accessToken = jwt.sign(
{
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey",
{ expiresIn: "24h" }
);
// Generate JWT refreshToken with 24h expiry
const refreshToken = jwt.sign(
{
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
{ expiresIn: "30d" }
);
await VAModel.updateOne({ "userDetails.email": userDetails.email }, {
$set: { accessToken, refreshToken }
})
// 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,
},
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
async function va_logout_callback(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const vaId = req.body.vaId || {};
const vaEmail = req.body.vaEmail || {};
if (!vaId || !vaEmail) return res.status(400).json({ success: false, message: "Invalid Credentials" });
// Find user by email
const user = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" });
}
await VAModel.updateOne({ "userDetails.email": vaEmail }, {
$set: { accessToken: null, refreshToken: null }
})
// Return accessToken and user info
return res.status(200).json({
success: true,
message: "Log Out Successfull",
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Refresh Token VA
async function va_refresh_token_callback(req, res) {
try {
const { refreshToken } = req.body || {};
if (!refreshToken) {
return res.status(400).json({
success: false,
message: "Refresh token missing",
});
}
// 🔍 Verify refresh token
jwt.verify(
refreshToken,
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
async (err, decoded) => {
if (err) {
return res.status(403).json({
success: false,
message: "Invalid or expired refresh token",
});
}
// Check if user still exists
const user = await VAModel.findById(decoded.id);
if (!user) {
return res.status(404).json({
success: false,
message: "User no longer exists",
});
}
// Generate a new access token
const newAccessToken = jwt.sign(
{
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
},
process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerAccessKey",
{ expiresIn: "24h" }
);
return res.status(200).json({
success: true,
message: "New access token generated",
accessToken: newAccessToken,
});
}
);
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Getting VA details by username
async function va_username_callback(req, res) {
try {
const { userName } = req.params;
if (!userName) {
return res.status(400).json({ success: false, message: "Missing userName param" });
}
// Find VA by common identifier fields (id, email, username, first/last name)
const va = await VAModel.findOne({
$or: [
{ _id: userName },
{ "userDetails.email": userName },
{ "userDetails.userName": userName },
{ "userDetails.firstName": userName },
{ "userDetails.lastName": userName },
{ "userDetails.fullName": userName }
]
}).lean().exec();
if (!va) {
return res.status(404).json({ success: false, message: "VA not found" });
}
// Latest log entry to determine active state
const lastLog = await LoggedModel.findOne({ vaId: va._id }).sort({ time: -1 }).lean().exec();
const active = lastLog ? String(lastLog.loggedType).toUpperCase() === "LOGIN" : false;
// Fetch all logs for this VA (ordered ascending) to compute logged hours
const logs = await LoggedModel.find({ vaId: va._id }).sort({ time: 1 }).lean().exec();
let totalSeconds = 0;
if (logs.length) {
// If logs have an explicit duration field, sum it
if (logs[0].duration !== undefined) {
totalSeconds = logs.reduce((sum, l) => sum + (Number(l.duration) || 0), 0);
} else {
// Otherwise compute from LOGIN/LOGOUT pairs
let lastLogin = null;
for (const l of logs) {
const t = l.time ? new Date(l.time) : null;
const type = String(l.loggedType || "").toUpperCase();
if (type === "LOGIN" && t) {
lastLogin = t;
} else if (type === "LOGOUT" && t && lastLogin) {
const diff = (t.getTime() - lastLogin.getTime()) / 1000;
if (diff > 0) totalSeconds += diff;
lastLogin = null;
}
}
// If currently logged in, add time since last login
if (lastLogin) {
totalSeconds += (Date.now() - lastLogin.getTime()) / 1000;
}
}
}
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
const totalHours = `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
// Project / client info (fall back to empty array)
const projects = va.clientIds || va.projects || [];
const name = `${va.userDetails?.firstName || ""}`.trim() + (va.userDetails?.lastName ? ` ${va.userDetails.lastName}` : "");
const email = va.userDetails?.email || null;
return res.status(200).json({
success: true,
data: {
id: va._id,
name: name || null,
email,
active,
totalHours,
lastLog: lastLog || null,
projects
}
});
} catch (error) {
console.error("va_username_callback error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Getting all clients or specific client by ID (with projects)
async function va_clients_callback(req, res) {
try {
const { clientID } = req.params;
if (clientID) {
// Find client by ID
const client = await ClientModel.findById(clientID).lean();
if (!client) {
return res.status(404).json({ success: false, message: "Client not found" });
}
// Find projects for this client
let projects = [];
if (client.projectIds && client.projectIds.length) {
projects = await ProjectModel.find({ _id: { $in: client.projectIds } }).lean();
}
return res.status(200).json({
success: true,
data: {
client,
projects
}
});
} else {
// Pagination: ?page=N (default 1), 20 per page
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = 20;
const skip = (page - 1) * limit;
const [clients, total] = await Promise.all([
ClientModel.find().skip(skip).limit(limit).lean(),
ClientModel.countDocuments()
]);
return res.status(200).json({
success: true,
page,
pageSize: clients.length,
total,
data: clients
});
}
} catch (error) {
console.error("va_clients_callback error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Getting projects for a specific client
async function va_clients_projects_callback(req, res) {
try {
const { clientId, projectId } = req.params;
return res.status(200).json({
success: true,
message: "VA Clients Projects Callback",
clientId,
projectId
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
async function va_log_callback(req, res) {
try {
const { projectId, from, to } = req.params;
const { vaId } = req.body;
if (!projectId) return res.status(400).json({ success: false, message: "Missing Props" })
const project = await ProjectModel.findById(projectId);
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
// Default range
const fromDate = from && from !== "undefined" ? new Date(from) : new Date(project.startDate || "2000-01-01");
const toDate = to && to !== "undefined" ? new Date(to) : new Date();
// If from and to are the same day
if (from === to) {
fromDate.setHours(0, 0, 0, 0);
toDate.setHours(23, 59, 59, 999);
} else {
// Otherwise use them as a normal range
toDate.setHours(23, 59, 59, 999);
}
const query = {
vaId: vaId,
projectId,
time: {
$gte: fromDate,
$lte: toDate
}
};
const logs = await LoggedModel.find(query).sort({ time: 1 });
if (!logs.length) {
return res.json({ success: true, message: "No logs found", totalWork: "0h 0m" });
}
// Find first LOGIN and last LOGOUT
const firstLogin = logs.find((l) => l.loggedType === "LOGIN");
const lastLogout = [...logs].reverse().find((l) => l.loggedType === "LOGOUT");
if (!firstLogin || !lastLogout) {
return res.json({
success: true,
message: "Incomplete log data (missing LOGIN or LOGOUT)",
totalWork: "0h 0m",
firstLogin,
lastLogout,
});
}
// Calculate duration
const diffMs = new Date(lastLogout.time) - new Date(firstLogin.time);
const totalMinutes = Math.floor(diffMs / (1000 * 60));
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
res.json({
success: true,
vaId,
projectId,
firstLogin: firstLogin.time,
lastLogout: lastLogout.time,
logs: logs,
totalTimeLogged: `${hours}h ${minutes}m`,
hoursLogged: hours,
minutesLogged: minutes,
});
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: err.message });
}
};
// 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");
await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
return res.status(201).send({ success: true, message: `${type} Successfully` });
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
async function va_time_logged_on_proj(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");
await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
return res.status(201).send({ success: true, message: `${type} Successfully` });
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
// Getting project of details (Clients, VAs, Logs)
async function va_projects_callback(req, res) {
try {
const { projectId } = req.params;
return res.status(200).json({
success: true,
message: "VA Project Callback",
projectId
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// New: VA filter search
// Query params supported: name, email, from, to
// - name: partial match against firstName or lastName (case-insensitive)
// - email: partial or full match against userDetails.email (case-insensitive)
// - from, to: ISO date strings (e.g. 2025-01-01) to filter logs by time
async function va_filter_search(req, res) {
try {
const { name, email, from, to } = req.query;
// Build VA query
const query = {};
const and = [];
if (name) {
const nameRegex = new RegExp(name, "i");
and.push({ $or: [{ "userDetails.firstName": nameRegex }, { "userDetails.lastName": nameRegex }] });
}
if (email) {
const emailRegex = new RegExp(email, "i");
and.push({ "userDetails.email": emailRegex });
}
if (and.length) query.$and = and;
// Find matching VAs
const vas = await VAModel.find(query).lean().exec();
// If no date range provided, return matched VAs only
if (!from && !to) {
return res.status(200).json({ success: true, count: vas.length, data: vas });
}
// Parse date range
const dateFilter = {};
if (from) {
const fromDate = new Date(from);
if (!isNaN(fromDate)) dateFilter.$gte = fromDate;
}
if (to) {
const toDate = new Date(to);
if (!isNaN(toDate)) dateFilter.$lte = toDate;
}
// If dateFilter is empty, just return vas
if (Object.keys(dateFilter).length === 0) {
return res.status(200).json({ success: true, count: vas.length, data: vas });
}
// Fetch logs for matching VAs in a single query
const vaIds = vas.map((v) => v._id).filter(Boolean);
const logs = await LoggedModel.find({ vaId: { $in: vaIds }, time: dateFilter }).lean().exec();
// Group logs by VA id
const logsByVa = logs.reduce((acc, log) => {
const key = String(log.vaId);
if (!acc[key]) acc[key] = [];
acc[key].push(log);
return acc;
}, {});
// Attach logs to each VA (as `logs` property)
const results = vas.map((v) => ({ ...v, logs: logsByVa[String(v._id)] || [] }));
return res.status(200).json({ success: true, count: results.length, data: results });
} catch (error) {
console.error("va_filter_search error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export {
logVaTime,
va_callback,
va_username_callback,
va_clients_callback,
va_clients_projects_callback,
va_log_callback,
va_project_log_callback,
va_filter_search,
va_register,
va_projects_callback,
va_login_callback,
va_logout_callback,
va_refresh_token_callback,
};