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