diff --git a/public/css/dashboard.css b/public/css/dashboard.css index ce02b96..a816238 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -286,6 +286,10 @@ body { } } +.section-title { + margin-bottom: 20px; +} + .stat-card { background-color: var(--bg-card); border-radius: 0.75rem; @@ -354,6 +358,13 @@ body { gap: 1.5rem; } +.status-indicator:has(>.active) { + text-align: center; + padding: 4px; + background-color: rgba(51, 255, 0, 0.24); + border-radius: 20px; +} + @media (min-width: 1024px) { .content-grid { grid-template-columns: repeat(2, 1fr); diff --git a/src/controllers/client/dashboard/clientDashboard.controller.js b/src/controllers/client/dashboard/clientDashboard.controller.js new file mode 100644 index 0000000..5dcd6f9 --- /dev/null +++ b/src/controllers/client/dashboard/clientDashboard.controller.js @@ -0,0 +1,311 @@ +import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js"; +import jwt from "jsonwebtoken"; + +// Getting client dashboard data - all VAs, projects, and active VAs +async function client_dashboard(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 clientEmail = decoded.email; + const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail }); + if (!clientUser) { + return res.status(404).json({ success: false, message: "Client not found" }); + } + + const clientID = clientUser._id; + + // Get all VA IDs and Project IDs assigned to this client + const clientVaIds = clientUser.vaIds || []; + const clientProjectIds = clientUser.projectIds || []; + + // Get all VAs assigned to this client (with full details) + const allAssignedVAs = await VAModel.find({ + _id: { $in: clientVaIds } + }) + .select('userDetails.firstName userDetails.lastName userDetails.email userDetails.phone status createdAt clientIds projectIds') + .lean() + .exec(); + + // Get All Projects of client + const clientProjects = await ProjectModel.find({ + _id: { $in: clientProjectIds } + }) + .select('name hourlyRate totalLoggedHours vaAssigned dedicatedHours createdAt clientId') + .lean() + .exec(); + + // Create project map for easy lookup + const projectMap = clientProjects.reduce((acc, project) => { + acc[project._id.toString()] = project; + return acc; + }, {}); + + // Find the latest log entry for each VA that belongs to this client and their projects + const agg = await LoggedModel.aggregate([ + { + $match: { + vaId: { $in: clientVaIds }, + projectId: { $in: clientProjectIds } + } + }, + { $sort: { time: -1 } }, + { + $group: { + _id: "$vaId", + lastLog: { $first: "$$ROOT" }, + totalLogins: { + $sum: { + $cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0] + } + }, + lastActivity: { $first: "$time" }, + projectsWorked: { + $addToSet: "$projectId" + }, + // Get all login logs for this VA + allLogs: { $push: "$$ROOT" } + } + }, + { + $match: { + "lastLog.loggedType": "LOGIN" + } + } + ]); + + const activeVaIds = agg.map((a) => a._id).filter(Boolean); + + // Get active VA details + const activeVAs = await VAModel.find({ + _id: { $in: activeVaIds }, + clientIds: clientID + }) + .select('userDetails.firstName userDetails.lastName userDetails.email userDetails.phone status createdAt projectIds') + .lean() + .exec(); + + // Create mapping for last log and activity data + const activityByVa = agg.reduce((acc, a) => { + acc[String(a._id)] = { + lastLog: a.lastLog, + totalLogins: a.totalLogins, + lastActivity: a.lastActivity, + projectsWorked: a.projectsWorked || [] + }; + return acc; + }, {}); + + // Calculate session duration for currently active VAs + const currentTime = new Date(); + + // Format all assigned VAs + const allVAsFormatted = allAssignedVAs.map((v) => { + const activity = activityByVa[String(v._id)] || {}; + const lastLog = activity.lastLog; + + // Calculate current session duration if still logged in + let currentSessionDuration = null; + let isCurrentlyActive = false; + let currentSessionFormatted = null; + let currentProjectDetails = null; + + if (lastLog && lastLog.loggedType === "LOGIN") { + const loginTime = new Date(lastLog.time); + currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); + currentSessionFormatted = formatTime(currentSessionDuration); + isCurrentlyActive = true; + + // Consider VA inactive if last login was more than 24 hours ago + const hoursSinceLastActivity = (currentTime - loginTime) / (1000 * 60 * 60); + if (hoursSinceLastActivity > 24) { + isCurrentlyActive = false; + } + + // Get current project details + if (lastLog.projectId && projectMap[lastLog.projectId.toString()]) { + currentProjectDetails = { + id: lastLog.projectId, + name: projectMap[lastLog.projectId.toString()].name + }; + } + } + + return { + id: v._id, + firstName: v.userDetails?.firstName || '', + lastName: v.userDetails?.lastName || '', + email: v.userDetails?.email, + phone: v.userDetails?.phone, + status: v.status || 'active', + isCurrentlyActive: isCurrentlyActive, + lastLoginTime: lastLog?.time || null, + lastActivity: activity.lastActivity, + currentSessionDuration: currentSessionDuration, + currentSessionFormatted: currentSessionFormatted, + totalLogins: activity.totalLogins || 0, + lastProject: currentProjectDetails, // Now with project details + lastProjectId: lastLog?.projectId || null, + projectsWorked: activity.projectsWorked || [], + projectsAssigned: v.projectIds?.length || 0, + vaSince: v.createdAt + }; + }); + + // Format active VAs (currently working) + const activeVAsFormatted = activeVAs.map((v) => { + const activity = activityByVa[String(v._id)] || {}; + const lastLog = activity.lastLog; + + let currentSessionDuration = null; + let currentSessionFormatted = null; + let currentProjectDetails = null; + + if (lastLog && lastLog.loggedType === "LOGIN") { + const loginTime = new Date(lastLog.time); + currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); + currentSessionFormatted = formatTime(currentSessionDuration); + + // Get current project details + if (lastLog.projectId && projectMap[lastLog.projectId.toString()]) { + currentProjectDetails = { + id: lastLog.projectId, + name: projectMap[lastLog.projectId.toString()].name + }; + } + } + + return { + id: v._id, + firstName: v.userDetails?.firstName || '', + lastName: v.userDetails?.lastName || '', + email: v.userDetails?.email, + phone: v.userDetails?.phone, + status: v.status || 'active', + lastLoginTime: lastLog?.time || null, + currentSessionDuration: currentSessionDuration, + currentSessionFormatted: currentSessionFormatted, + totalLogins: activity.totalLogins || 0, + currentProject: currentProjectDetails, + currentProjectId: lastLog?.projectId || null, + projectsAssigned: v.projectIds?.length || 0, + vaSince: v.createdAt + }; + }); + + // Format projects with additional data + const projectsFormatted = clientProjects.map((p) => { + // Find VAs currently working on this project + const activeVAsOnProject = allVAsFormatted.filter(va => + va.isCurrentlyActive && va.lastProjectId && + va.lastProjectId.toString() === p._id.toString() + ); + + // Find all VAs assigned to this project + const assignedVAs = allAssignedVAs.filter(va => + va.projectIds && va.projectIds.some(projectId => + projectId.toString() === p._id.toString() + ) + ); + + return { + id: p._id, + name: p.name, + hourlyRate: p.hourlyRate, + totalLoggedHours: p.totalLoggedHours || 0, + dedicatedHours: p.dedicatedHours || 0, + vaAssigned: assignedVAs.length, + vaCurrentlyWorking: activeVAsOnProject.length, + activeVAs: activeVAsOnProject.map(va => ({ + id: va.id, + name: va.name, + currentSession: va.currentSessionFormatted, + lastLoginTime: va.lastLoginTime + })), + createdAt: p.createdAt + }; + }); + + // Calculate dashboard statistics + const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0); + const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0); + + const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive); + const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive); + + // Calculate projects with active workers + const projectsWithActiveWorkers = projectsFormatted.filter(p => p.vaCurrentlyWorking > 0).length; + + return res.status(200).json({ + success: true, + data: { + // All assigned VAs with their status + allVAs: { + active: currentlyActiveVAs, + inactive: inactiveVAs, + total: allVAsFormatted + }, + // Currently working VAs (active session) + activeVAs: activeVAsFormatted, + // All client projects + projects: projectsFormatted + }, + summary: { + totalVAs: allAssignedVAs.length, + activeVAs: currentlyActiveVAs.length, + inactiveVAs: inactiveVAs.length, + totalProjects: clientProjects.length, + projectsWithActiveWorkers: projectsWithActiveWorkers, + totalHoursLogged: totalHoursLogged, + totalDedicatedHours: totalDedicatedHours, + utilizationRate: totalDedicatedHours > 0 ? + ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%', + lastUpdated: currentTime.toISOString() + }, + counts: { + vas: { + total: allAssignedVAs.length, + active: currentlyActiveVAs.length, + inactive: inactiveVAs.length + }, + projects: clientProjects.length, + activeSessions: activeVAsFormatted.length, + projectsWithActivity: projectsWithActiveWorkers + } + }); + + } catch (error) { + console.error("Client dashboard error:", error); + return res.status(500).json({ + success: false, + message: "Internal Server Error", + error: error.message + }); + } +} + +// Helper function to format time +function formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + if (hours > 0) { + return `${hours}h ${minutes}m ${remainingSeconds}s`; + } else if (minutes > 0) { + return `${minutes}m ${remainingSeconds}s`; + } else { + return `${remainingSeconds}s`; + } +} + +export { client_dashboard }; \ No newline at end of file diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 36b2ff5..ee688f6 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -46,6 +46,55 @@ async function authValidator(req, res, next) { } } -export { - authValidator +async function clientAuthValidator(req, res, next) { + try { + const accessToken = req.headers.token; + + if (!accessToken) { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (decoded && decoded.id && decoded.email) { + const clientEmail = decoded.email; + + const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail }); + + if (!clientUser) { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + + if (!clientUser.accessToken || clientUser.accessToken !== accessToken) { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + + // Attach client info to request object for use in subsequent middleware/controllers + req.body.clientId = clientUser._id; + req.body.clientEmail = clientEmail; + req.body.clientName = decoded.name; + + next(); + } else { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + + } catch (error) { + logger.error("clientAuthValidator - Middleware (CATCH Block)", error.message); + + if (error.name === "TokenExpiredError") { + return res.status(401).json({ success: false, message: "Token expired" }); + } + + if (error.name === "JsonWebTokenError") { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + return res.status(401).json({ success: false, message: "Authentication failed" }); + } +} + +export { + authValidator, + clientAuthValidator } diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 127df7f..960906a 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -2,11 +2,13 @@ import express from "express"; const clientRoutes = express.Router(); import { va_register_client } from "../../controllers/client/vaClient.controller.js"; -import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.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_login_client } from "../../controllers/client/vaClientLogin.controller.js"; import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.controller.js"; import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js"; +import { clientAuthValidator } from "../../middlewares/auth.middleware.js"; +import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js"; // clients routes clientRoutes.post("/register", va_register_client); // Register a new VA @@ -14,11 +16,16 @@ clientRoutes.post("/login", va_login_client); // Login Client clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token clientRoutes.post("/forgot-password", client_request_password_reset); -clientRoutes.get("/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC) +// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) +// dashboard API's +clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback (NC) + + + export default clientRoutes; \ No newline at end of file diff --git a/src/server.js b/src/server.js index 89dbe86..f13aaac 100644 --- a/src/server.js +++ b/src/server.js @@ -63,8 +63,9 @@ app.get("/client/forgot-password", (req, res) => { // Client Dashboard Routes with new file names app.get('/client/dashboard', async (req, res) => { try { - const dashboardData = await dashboardService.getClientDashboard(req.user.id); - res.render('client/dashboard-dashboard', dashboardData); + // const dashboardData = await dashboardService.getClientDashboard(req.user.id); + // res.render('client/dashboard-dashboard', dashboardData); + res.render('client/dashboard-dashboard'); } catch (error) { console.error('Dashboard error:', error); res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); @@ -73,8 +74,9 @@ app.get('/client/dashboard', async (req, res) => { app.get('/client/virtual-assistants', async (req, res) => { try { - const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-virtual-assistants', vaData); + // const vaData = await virtualAssistantService.getClientVAs(req.user.id); + res.render('client/dashboard-virtual-assistants'); + // res.render('client/dashboard-virtual-assistants', vaData); } catch (error) { console.error('Virtual assistants error:', error); res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' }); @@ -111,7 +113,7 @@ app.get('/client/virtual-assistants', async (req, res) => { // } // }); -app.get("/live/:projectId", (req, res) => { +app.get("/live/:projectId/:vaId", (req, res) => { const { projectId } = req.params; res.render("va_live_tracker", { title: "VA Live Tracker", projectId }) }) diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index c05be5a..091dc6b 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -24,16 +24,16 @@ Dashboard - +
TT
@@ -73,7 +73,7 @@ Virtual Assistants - - -