397 lines
16 KiB
JavaScript
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 }; |