Compare commits
2 Commits
8d4a23d2c4
...
e55183a2ef
| Author | SHA1 | Date |
|---|---|---|
|
|
e55183a2ef | |
|
|
4915c04372 |
|
|
@ -1,7 +1,4 @@
|
||||||
import bcrypt from "bcrypt";
|
import { LoggedModel } from "../models/vaModel.js";
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../models/vaModel.js";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function logVaTime(req, res) {
|
async function logVaTime(req, res) {
|
||||||
|
|
@ -18,485 +15,8 @@ async function logVaTime(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
async function va_time_logged_on_proj(req, res) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -512,104 +32,10 @@ async function va_time_logged_on_proj(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
export {
|
||||||
logVaTime,
|
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,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { VAModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_login_callback };
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { VAModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_logout_callback };
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { VAModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
// 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" }
|
||||||
|
);
|
||||||
|
|
||||||
|
await VAModel.updateOne({ "userDetails.email": user.userDetails.email }, {
|
||||||
|
$set: { accessToken: newAccessToken }
|
||||||
|
})
|
||||||
|
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_refresh_token_callback };
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_clients_projects_callback };
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { ClientModel, ProjectModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { va_clients_callback };
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { VAModel, LoggedModel } from "../../models/vaModel.js";
|
||||||
|
|
||||||
|
|
||||||
|
// 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 { va_filter_search };
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { VAModel, LoggedModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_callback };
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { LoggedModel, ProjectModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { va_log_callback };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import { VAModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check if user already exists by email
|
||||||
|
const existingUser = await VAModel.findOne({ "userDetails.email": userDetails.email.toLowerCase() });
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(409).json({ success: false, message: "User already exists with this email" });
|
||||||
|
}
|
||||||
|
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_register };
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { LoggedModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).send("Internal Server Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_project_log_callback };
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_projects_callback };
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { VAModel, LoggedModel } from "../../models/vaModel.js";
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { va_username_callback };
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,54 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
const vaRoutes = express.Router();
|
const vaRoutes = express.Router();
|
||||||
|
|
||||||
import { va_callback, va_username_callback, va_clients_callback, va_clients_projects_callback, va_log_callback, va_project_log_callback, va_filter_search, va_projects_callback, va_register, va_login_callback, va_refresh_token_callback, va_logout_callback } from "../../controllers/va.controller.js";
|
import { va_username_callback } from "../../controllers/va/vaUsername.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";
|
||||||
|
import { va_filter_search } from "../../controllers/va/vaFilterSearch.controller.js";
|
||||||
|
import { va_log_callback } from "../../controllers/va/vaLog/vaLog.controller.js";
|
||||||
|
import { va_callback } from "../../controllers/va/vaLog/vaListLog.controller.js";
|
||||||
|
import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
|
||||||
|
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.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 { 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`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue