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

397 lines
16 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" },
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 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 using the same logic as va_log_work_report_callback
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]; // YYYY-MM-DD
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;
let lastProjectId = 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;
lastProjectId = log.projectId?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
// reset after each pair
lastLogin = null;
lastProjectId = null;
}
}
totalWorkSeconds += dayWorkSeconds;
}
totalWorkedTimeByVa[vaId] = {
totalSeconds: totalWorkSeconds,
formatted: formatTime(totalWorkSeconds)
};
}
// 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 workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
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
};
}
}
// Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
return {
id: v._id,
name: fullName,
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,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
totalLogins: activity.totalLogins || 0,
lastProject: currentProjectDetails,
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 workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
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
};
}
}
// Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
return {
id: v._id,
name: fullName,
email: v.userDetails?.email,
phone: v.userDetails?.phone,
status: v.status || 'active',
lastLoginTime: lastLog?.time || null,
currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
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 total worked time across all VAs
const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0);
// 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,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
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 (same as in va_log_work_report_callback)
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 };