364 lines
14 KiB
JavaScript
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 }; |