diff --git a/src/controllers/va.controller.js b/src/controllers/va.controller.js index 43dcdd4..aae84fe 100644 --- a/src/controllers/va.controller.js +++ b/src/controllers/va.controller.js @@ -1,7 +1,4 @@ -import bcrypt from "bcrypt"; -import jwt from "jsonwebtoken"; -import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../models/vaModel.js"; - +import { LoggedModel } from "../models/vaModel.js"; 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": userDetails.email }, { - $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 { @@ -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 { - 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, + logVaTime }; diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js new file mode 100644 index 0000000..625280f --- /dev/null +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaAuth/vaLogout.controller.js b/src/controllers/va/vaAuth/vaLogout.controller.js new file mode 100644 index 0000000..8df33b9 --- /dev/null +++ b/src/controllers/va/vaAuth/vaLogout.controller.js @@ -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": userDetails.email }, { + $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 }; \ No newline at end of file diff --git a/src/controllers/va/vaAuth/vaRefreshToken.controller.js b/src/controllers/va/vaAuth/vaRefreshToken.controller.js new file mode 100644 index 0000000..c79b36d --- /dev/null +++ b/src/controllers/va/vaAuth/vaRefreshToken.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaAuth/vaRegister.controller.js b/src/controllers/va/vaAuth/vaRegister.controller.js new file mode 100644 index 0000000..9354858 --- /dev/null +++ b/src/controllers/va/vaAuth/vaRegister.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaClients/vaClientProject.controller.js b/src/controllers/va/vaClients/vaClientProject.controller.js new file mode 100644 index 0000000..9eb7902 --- /dev/null +++ b/src/controllers/va/vaClients/vaClientProject.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaClients/vaClients.controller.js b/src/controllers/va/vaClients/vaClients.controller.js new file mode 100644 index 0000000..1c48ec1 --- /dev/null +++ b/src/controllers/va/vaClients/vaClients.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaFilterSearch.controller.js b/src/controllers/va/vaFilterSearch.controller.js new file mode 100644 index 0000000..602690c --- /dev/null +++ b/src/controllers/va/vaFilterSearch.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaLog/vaListLog.controller.js b/src/controllers/va/vaLog/vaListLog.controller.js new file mode 100644 index 0000000..2b60b70 --- /dev/null +++ b/src/controllers/va/vaLog/vaListLog.controller.js @@ -0,0 +1,37 @@ +import { VAModel, LoggedModel, ClientModel, ProjectModel } 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 }; \ No newline at end of file diff --git a/src/controllers/va/vaLog/vaLog.controller.js b/src/controllers/va/vaLog/vaLog.controller.js new file mode 100644 index 0000000..46fe13c --- /dev/null +++ b/src/controllers/va/vaLog/vaLog.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js new file mode 100644 index 0000000..f4d79f0 --- /dev/null +++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js @@ -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"); + 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"); + } +} + +export { va_project_log_callback }; \ No newline at end of file diff --git a/src/controllers/va/vaProjects/vaProjects.controller.js b/src/controllers/va/vaProjects/vaProjects.controller.js new file mode 100644 index 0000000..41a52db --- /dev/null +++ b/src/controllers/va/vaProjects/vaProjects.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaUsername.controller.js b/src/controllers/va/vaUsername.controller.js new file mode 100644 index 0000000..72af821 --- /dev/null +++ b/src/controllers/va/vaUsername.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index f947f33..86f9e3b 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -1,7 +1,18 @@ import express from "express"; 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/vaAuth/vaRegister.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"; vaRoutes.get("/", va_callback); // VA Callback for getting active VA