va-tracker-middleware/src/controllers/client/dashboard/clientDashboard.controller.js

364 lines
14 KiB
JavaScript

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" }
}
},
{
$match: {
"lastLog.loggedType": "LOGIN"
}
}
]);
const activeVaIds = agg.map((a) => a._id).filter(Boolean);
// 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
};
return acc;
}, {});
// Calculate total worked time for each VA
const totalWorkedTimeByVa = {};
// Get all logs for client's VAs to calculate total worked time
const allLogs = await LoggedModel.find({
vaId: { $in: clientVaIds },
projectId: { $in: clientProjectIds }
})
.sort({ time: 1 })
.lean();
// Group logs by VA
const logsByVa = {};
allLogs.forEach(log => {
const vaId = log.vaId.toString();
if (!logsByVa[vaId]) {
logsByVa[vaId] = [];
}
logsByVa[vaId].push(log);
});
// Calculate total worked time for each VA
for (const [vaId, logs] of Object.entries(logsByVa)) {
let totalWorkSeconds = 0;
// Group logs by date (YYYY-MM-DD format)
const logsByDate = {};
logs.forEach(log => {
const date = new Date(log.time).toISOString().split('T')[0];
if (!logsByDate[date]) {
logsByDate[date] = [];
}
logsByDate[date].push(log);
});
// Process each day separately
for (const [date, dayLogs] of Object.entries(logsByDate)) {
// Check if this day has any adjustment logs
const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
let dayWorkSeconds = 0;
let lastLogin = null;
// If day has adjustment logs, only process adjustment logs
// If no adjustment logs, process all logs for that day
const logsToProcess = hasAdjustmentLogs
? dayLogs.filter(log => log.isAdjustment === true)
: dayLogs;
// Sort logs by time for proper pairing
logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
// Calculate work duration for this day
for (const log of logsToProcess) {
const time = new Date(log.time);
if (log.loggedType === "LOGIN") {
lastLogin = time;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
lastLogin = null;
}
}
totalWorkSeconds += dayWorkSeconds;
}
totalWorkedTimeByVa[vaId] = {
totalSeconds: totalWorkSeconds,
formatted: formatTime(totalWorkSeconds)
};
}
// NEW: Calculate project-wise work time for each VA
const projectWorkTimeByVa = {};
for (const [vaId, logs] of Object.entries(logsByVa)) {
const projectWorkTime = {};
// Group logs by project
const logsByProject = {};
logs.forEach(log => {
const projectId = log.projectId?.toString();
if (projectId) {
if (!logsByProject[projectId]) {
logsByProject[projectId] = [];
}
logsByProject[projectId].push(log);
}
});
// Calculate work time per project
for (const [projectId, projectLogs] of Object.entries(logsByProject)) {
let projectWorkSeconds = 0;
// Group logs by date
const logsByDate = {};
projectLogs.forEach(log => {
const date = new Date(log.time).toISOString().split('T')[0];
if (!logsByDate[date]) {
logsByDate[date] = [];
}
logsByDate[date].push(log);
});
// Process each day separately
for (const [date, dayLogs] of Object.entries(logsByDate)) {
const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
let dayWorkSeconds = 0;
let lastLogin = null;
const logsToProcess = hasAdjustmentLogs
? dayLogs.filter(log => log.isAdjustment === true)
: dayLogs;
logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
for (const log of logsToProcess) {
const time = new Date(log.time);
if (log.loggedType === "LOGIN") {
lastLogin = time;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
lastLogin = null;
}
}
projectWorkSeconds += dayWorkSeconds;
}
projectWorkTime[projectId] = {
totalSeconds: projectWorkSeconds,
formatted: formatTime(projectWorkSeconds),
projectName: projectMap[projectId]?.name || 'Unknown Project'
};
}
projectWorkTimeByVa[vaId] = projectWorkTime;
}
// Calculate session duration for currently active VAs
const currentTime = new Date();
// Format all assigned VAs - Only include data needed for frontend
const allVAsFormatted = allAssignedVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog;
const projectWorkTime = projectWorkTimeByVa[String(v._id)] || {};
// Calculate current session duration if still logged in
let currentSessionDuration = null;
let isCurrentlyActive = false;
let currentProjectDetails = null;
if (lastLog && lastLog.loggedType === "LOGIN") {
const loginTime = new Date(lastLog.time);
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
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
};
}
}
// Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
// Filter: Only include projects that belong to this client
const clientProjectsAssigned = (v.projectIds || []).filter(projectId =>
clientProjectIds.some(clientProjectId =>
clientProjectId.toString() === projectId.toString()
)
);
// Format project work time for popup
const projectsWithWorkTime = clientProjectsAssigned.map(projectId => {
const projectIdStr = projectId.toString();
const workData = projectWorkTime[projectIdStr] || { totalSeconds: 0, formatted: '00:00:00' };
const project = projectMap[projectIdStr];
return {
id: projectId,
name: project?.name || 'Unknown Project',
totalWorkedTime: workData.formatted,
totalWorkedSeconds: workData.totalSeconds,
isCurrentlyWorking: currentProjectDetails?.id?.toString() === projectIdStr,
hourlyRate: project?.hourlyRate || 0
};
});
return {
id: v._id,
name: fullName,
email: v.userDetails?.email,
phone: v.userDetails?.phone,
status: v.status || 'active',
isCurrentlyActive: isCurrentlyActive,
lastLoginTime: lastLog?.time || null,
currentSessionDuration: currentSessionDuration,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
lastProject: currentProjectDetails,
lastProjectId: lastLog?.projectId || null,
projectsAssigned: clientProjectsAssigned.length,
// Detailed project information for popup
projects: projectsWithWorkTime,
vaSince: v.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 total worked time across all VAs
const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0);
return res.status(200).json({
success: true,
data: {
// Only send data that frontend actually uses
allVAs: allVAsFormatted
},
summary: {
totalVAs: allAssignedVAs.length,
activeVAs: currentlyActiveVAs.length,
inactiveVAs: inactiveVAs.length,
totalProjects: clientProjects.length,
totalHoursLogged: totalHoursLogged,
totalDedicatedHours: totalDedicatedHours,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
utilizationRate: totalDedicatedHours > 0 ?
((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%'
}
});
} 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 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")}`;
}
export { client_dashboard };