This commit is contained in:
=bhagwanSingh 2025-11-18 11:24:56 +05:30
commit acb1061de5
14 changed files with 1656 additions and 213 deletions

View File

@ -644,3 +644,305 @@ body {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
} }
} }
/* Popup Styles */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.popup-content {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: popupFadeIn 0.3s ease-out;
}
@keyframes popupFadeIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.popup-header h3 {
margin: 0;
color: #1f2937;
font-size: 1.25rem;
font-weight: 600;
}
.popup-close {
background: none;
border: none;
font-size: 1.25rem;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.2s;
}
.popup-close:hover {
background: #f3f4f6;
color: #374151;
}
.popup-body {
padding: 1.5rem;
}
.va-details-grid {
display: grid;
gap: 1rem;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #f3f4f6;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-item label {
font-weight: 600;
color: #374151;
font-size: 0.875rem;
}
.detail-item span {
color: #6b7280;
text-align: right;
}
.status-active {
color: #059669;
font-weight: 600;
}
.status-inactive {
color: #dc2626;
font-weight: 600;
}
.popup-footer {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-top: 1px solid #e5e7eb;
justify-content: flex-end;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
font-size: 0.875rem;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
/* Make table rows clickable */
tr[data-vaid] {
cursor: pointer;
transition: background-color 0.2s;
}
tr[data-vaid]:hover {
background-color: #f8fafc !important;
}
/* Ensure view link doesn't trigger row click */
.view-link {
cursor: pointer;
z-index: 10;
position: relative;
}
/* Project Details Styles in Popup */
.projects-section {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid #e5e7eb;
}
.projects-title {
font-size: 1rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 1rem 0;
}
.projects-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.project-item {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
transition: all 0.2s;
margin-top: 12px;
}
.project-item:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.project-name {
font-weight: 600;
color: #1f2937;
font-size: 0.9rem;
}
.project-time {
background: #3b82f6;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
}
.project-id {
font-size: 0.75rem;
color: #6b7280;
font-family: monospace;
}
.no-projects {
text-align: center;
color: #6b7280;
font-style: italic;
padding: 1rem;
background: #f8fafc;
border-radius: 6px;
border: 1px dashed #d1d5db;
}
/* Adjust basic info layout */
.va-basic-info {
display: grid;
gap: 1rem;
margin-bottom: 1rem;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f3f4f6;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-item label {
font-weight: 600;
color: #374151;
font-size: 0.875rem;
}
.detail-item span {
color: #6b7280;
text-align: right;
}
.status-active {
color: #059669;
font-weight: 600;
}
.status-inactive {
color: #dc2626;
font-weight: 600;
}
/* Popup responsive adjustments */
@media (max-width: 480px) {
.project-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.project-time {
align-self: flex-start;
}
.detail-item {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.detail-item span {
text-align: left;
}
}

View File

@ -105,12 +105,92 @@ async function client_dashboard(req, res) {
return acc; 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 // Calculate session duration for currently active VAs
const currentTime = new Date(); const currentTime = new Date();
// Format all assigned VAs // Format all assigned VAs
const allVAsFormatted = allAssignedVAs.map((v) => { const allVAsFormatted = allAssignedVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {}; const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog; const lastLog = activity.lastLog;
// Calculate current session duration if still logged in // Calculate current session duration if still logged in
@ -140,10 +220,12 @@ async function client_dashboard(req, res) {
} }
} }
// Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
return { return {
id: v._id, id: v._id,
firstName: v.userDetails?.firstName || '', name: fullName,
lastName: v.userDetails?.lastName || '',
email: v.userDetails?.email, email: v.userDetails?.email,
phone: v.userDetails?.phone, phone: v.userDetails?.phone,
status: v.status || 'active', status: v.status || 'active',
@ -152,8 +234,10 @@ async function client_dashboard(req, res) {
lastActivity: activity.lastActivity, lastActivity: activity.lastActivity,
currentSessionDuration: currentSessionDuration, currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted, currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
totalLogins: activity.totalLogins || 0, totalLogins: activity.totalLogins || 0,
lastProject: currentProjectDetails, // Now with project details lastProject: currentProjectDetails,
lastProjectId: lastLog?.projectId || null, lastProjectId: lastLog?.projectId || null,
projectsWorked: activity.projectsWorked || [], projectsWorked: activity.projectsWorked || [],
projectsAssigned: v.projectIds?.length || 0, projectsAssigned: v.projectIds?.length || 0,
@ -164,6 +248,7 @@ async function client_dashboard(req, res) {
// Format active VAs (currently working) // Format active VAs (currently working)
const activeVAsFormatted = activeVAs.map((v) => { const activeVAsFormatted = activeVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {}; const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog; const lastLog = activity.lastLog;
let currentSessionDuration = null; let currentSessionDuration = null;
@ -184,16 +269,20 @@ async function client_dashboard(req, res) {
} }
} }
// Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
return { return {
id: v._id, id: v._id,
firstName: v.userDetails?.firstName || '', name: fullName,
lastName: v.userDetails?.lastName || '',
email: v.userDetails?.email, email: v.userDetails?.email,
phone: v.userDetails?.phone, phone: v.userDetails?.phone,
status: v.status || 'active', status: v.status || 'active',
lastLoginTime: lastLog?.time || null, lastLoginTime: lastLog?.time || null,
currentSessionDuration: currentSessionDuration, currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted, currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
totalLogins: activity.totalLogins || 0, totalLogins: activity.totalLogins || 0,
currentProject: currentProjectDetails, currentProject: currentProjectDetails,
currentProjectId: lastLog?.projectId || null, currentProjectId: lastLog?.projectId || null,
@ -242,6 +331,9 @@ async function client_dashboard(req, res) {
const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive); const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive);
const inactiveVAs = 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 // Calculate projects with active workers
const projectsWithActiveWorkers = projectsFormatted.filter(p => p.vaCurrentlyWorking > 0).length; const projectsWithActiveWorkers = projectsFormatted.filter(p => p.vaCurrentlyWorking > 0).length;
@ -267,6 +359,7 @@ async function client_dashboard(req, res) {
projectsWithActiveWorkers: projectsWithActiveWorkers, projectsWithActiveWorkers: projectsWithActiveWorkers,
totalHoursLogged: totalHoursLogged, totalHoursLogged: totalHoursLogged,
totalDedicatedHours: totalDedicatedHours, totalDedicatedHours: totalDedicatedHours,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
utilizationRate: totalDedicatedHours > 0 ? utilizationRate: totalDedicatedHours > 0 ?
((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%', ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%',
lastUpdated: currentTime.toISOString() lastUpdated: currentTime.toISOString()
@ -293,19 +386,12 @@ async function client_dashboard(req, res) {
} }
} }
// Helper function to format time // Helper function to format time (same as in va_log_work_report_callback)
function formatTime(seconds) { function formatTime(seconds) {
const hours = Math.floor(seconds / 3600); const h = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const m = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60; const s = Math.floor(seconds % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
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 }; export { client_dashboard };

View File

@ -0,0 +1,166 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel, ClientModel } from "../../../models/vaModel.js";
// Helper function to parse date string in DD-MM-YYYY format
function parseDate(dateString) {
if (!dateString) return null;
const parts = dateString.split('-');
if (parts.length !== 3) return null;
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // Months are 0-indexed in JavaScript
const year = parseInt(parts[2], 10);
// Validate date components
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
if (day < 1 || day > 31 || month < 0 || month > 11 || year < 1900) return null;
return new Date(year, month, day);
}
// Getting logged hours in a specific project of VA
async function client_dashboard_va_log(req, res) {
console.log(req)
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;
const clientProjectIds = clientUser.projectIds || [];
const payload = req.body || {};
const { vaId } = payload;
// const va = await VAModel.findOne({
// _id: vaId,
// clientIds: clientID
// });
// if (!va) {
// return res.status(403).json({
// success: false,
// message: "VA not found or not assigned to this client"
// });
// }
let logs = await LoggedModel.find({
vaId,
projectId: { $in: clientProjectIds },
})
.sort({ time: 1 })
.populate("projectId", "name") // attach project name
.lean();
if (!logs.length) {
return res.status(200).json({
success: true,
message: "No logs found in given range",
data: [],
totalWorkTime: "00:00:00",
});
}
let totalWorkSeconds = 0;
const projectWork = {}; // key: projectId -> seconds
// 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 (already sorted by query, but ensure)
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?._id?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
if (lastProjectId) {
projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec;
}
// reset after each pair
lastLogin = null;
lastProjectId = null;
}
}
totalWorkSeconds += dayWorkSeconds;
}
// 🕓 Convert total seconds to hh:mm:ss
const 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")}`;
};
// 🧠 Build project-wise array
const projectDetails = await Promise.all(
Object.entries(projectWork).map(async ([projectId, sec]) => {
const project = await ProjectModel.findById(projectId).lean();
return {
projectId,
projectName: project?.name || "Unknown Project",
totalTime: formatTime(sec),
};
})
);
return res.status(200).json({
success: true,
totalWorkTime: formatTime(totalWorkSeconds),
projects: projectDetails,
});
} catch (error) {
console.log(error)
return res.status(500).send("Internal Server Error");
}
}
export { client_dashboard_va_log };

View File

@ -0,0 +1,120 @@
import { ProjectModel, VAModel, ClientModel } from "../../models/vaModel.js";
import { Types } from "mongoose";
import { updateEntityRelationships } from "../../helper/updateEntityRelationships.js";
// Register a new VA
async function project_add_sf(req, res) {
try {
const payload = req.body|| {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload;
// Validate required fields
if (!name) {
return res.status(400).json({
success: false,
message: "Project name is required"
});
}
if (!clientId) {
return res.status(400).json({
success: false,
message: "Client ID is required"
});
}
// Validate and convert clientId to ObjectId
let clientObjectId;
try {
clientObjectId = new Types.ObjectId(clientId);
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid Client ID format"
});
}
// Validate and convert vaAssigned to array of ObjectIds
let vaAssignedArray = [];
if (vaAssigned) {
if (Array.isArray(vaAssigned)) {
try {
vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format in vaAssigned array"
});
}
} else if (typeof vaAssigned === 'string') {
// Handle single VA ID as string
try {
vaAssignedArray = [new Types.ObjectId(vaAssigned)];
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format"
});
}
}
}
// sanitize/construct allowed fields
const toCreate = {
name: name.trim(),
hourlyRate: hourlyRate || '0',
clientId: clientObjectId,
vaAssigned: vaAssignedArray,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
};
const created = await ProjectModel.create(toCreate);
const relationshipUpdate = await updateEntityRelationships({
project: {
projectId: created._id,
clientId: clientObjectId,
vaId: vaAssignedArray
}
});
if (!relationshipUpdate.success) {
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
}
// Send data to Salesforce API after successful registration
try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`;
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(created)
// });
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
// const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
// res.status(201).json({ success: true, data: created, SyncSalesforce: result });
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
} catch (error) {
console.error("❌ Failed to sync with Salesforce:", error.message);
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
} catch (error) {
console.error("project_add_sf error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { project_add_sf };

View File

@ -0,0 +1,217 @@
import { ProjectModel } from "../../models/vaModel.js";
import { Types } from "mongoose";
import { updateEntityRelationships, removeEntityRelationships } from "../../helper/updateEntityRelationships.js";
/**
* Update project details - PATCH endpoint
* Supports updating: status, VA assignments, hourly rate, dedicated hours, etc.
*/
async function update_project_sf(req, res) {
try {
const { projectId } = req.params;
const payload = req.body || {};
const {
name,
hourlyRate,
vaAssigned,
dedicatedHours,
status,
totalLoggedHours
} = payload;
// Validate projectId
if (!projectId) {
return res.status(400).json({
success: false,
message: "Project ID is required"
});
}
let projectObjectId;
try {
projectObjectId = new Types.ObjectId(projectId);
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid Project ID format"
});
}
// Check if project exists
const existingProject = await ProjectModel.findById(projectObjectId);
if (!existingProject) {
return res.status(404).json({
success: false,
message: "Project not found"
});
}
// Prepare update data
const updateData = {};
let relationshipUpdate = null;
let vaRelationshipUpdate = null;
// Update name if provided
if (name !== undefined) {
if (!name.trim()) {
return res.status(400).json({
success: false,
message: "Project name cannot be empty"
});
}
updateData.name = name.trim();
}
// Update hourly rate if provided
if (hourlyRate !== undefined) {
updateData.hourlyRate = hourlyRate;
}
// Update dedicated hours if provided
if (dedicatedHours !== undefined) {
updateData.dedicatedHours = parseFloat(dedicatedHours) || 0;
}
// Update total logged hours if provided
if (totalLoggedHours !== undefined) {
updateData.totalLoggedHours = parseFloat(totalLoggedHours) || 0;
}
// Update status if provided
if (status !== undefined) {
if (!["ACTIVE", "INACTIVE"].includes(status)) {
return res.status(400).json({
success: false,
message: "Status must be either ACTIVE or INACTIVE"
});
}
updateData.status = status;
}
// Handle VA assignments - this requires relationship updates
if (vaAssigned !== undefined) {
let newVaAssignedArray = [];
// Parse and validate VA IDs
if (vaAssigned && vaAssigned.length > 0) {
if (Array.isArray(vaAssigned)) {
try {
newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format in vaAssigned array"
});
}
} else if (typeof vaAssigned === 'string') {
try {
newVaAssignedArray = [new Types.ObjectId(vaAssigned)];
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid VA ID format"
});
}
}
}
updateData.vaAssigned = newVaAssignedArray;
// Calculate VAs to add and remove for relationship updates
const currentVaIds = existingProject.vaAssigned.map(va => va.toString());
const newVaIds = newVaAssignedArray.map(va => va.toString());
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString()));
// Update relationships for added VAs
if (vaToAdd.length > 0) {
relationshipUpdate = await updateEntityRelationships({
project: {
projectId: projectObjectId,
clientId: existingProject.clientId,
vaId: vaToAdd
}
});
}
// Remove relationships for removed VAs
if (vaToRemove.length > 0) {
vaRelationshipUpdate = await removeEntityRelationships({
project: {
projectId: projectObjectId,
clientId: existingProject.clientId,
vaId: vaToRemove
}
});
}
}
// If no fields to update
if (Object.keys(updateData).length === 0) {
return res.status(400).json({
success: false,
message: "No valid fields to update"
});
}
// Update the project
const updatedProject = await ProjectModel.findByIdAndUpdate(
projectObjectId,
updateData,
{ new: true, runValidators: true }
).populate('clientId', 'userDetails.name userDetails.email')
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
// If VA assignments were changed but relationship update failed, log warning
if (vaAssigned !== undefined) {
if (relationshipUpdate && !relationshipUpdate.success) {
console.warn('Project updated but VA relationship addition failed:', relationshipUpdate.message);
}
if (vaRelationshipUpdate && !vaRelationshipUpdate.success) {
console.warn('Project updated but VA relationship removal failed:', vaRelationshipUpdate.message);
}
}
return res.status(200).json({
success: true,
message: "Project updated successfully",
data: updatedProject,
relationships: {
added: relationshipUpdate,
removed: vaRelationshipUpdate
}
});
} catch (error) {
console.error("update_project_sf error:", error);
// MongoDB duplicate key error
if (error.code === 11000) {
return res.status(400).json({
success: false,
message: "Project with this name already exists"
});
}
// MongoDB validation error
if (error.name === 'ValidationError') {
const errors = Object.values(error.errors).map(err => err.message);
return res.status(400).json({
success: false,
message: "Validation error",
errors: errors
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export {
update_project_sf
};

View File

@ -85,35 +85,56 @@ async function va_log_work_report_callback(req, res) {
let totalWorkSeconds = 0; let totalWorkSeconds = 0;
const projectWork = {}; // key: projectId -> seconds const projectWork = {}; // key: projectId -> seconds
let lastLogin = null; // Group logs by date (YYYY-MM-DD format)
let lastProjectId = null; const logsByDate = {};
logs.forEach(log => {
const hasAdjustmentLogs = logs.some(log => log.isAdjustment === true); const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD
if (!logsByDate[date]) {
if(hasAdjustmentLogs) { logsByDate[date] = [];
logs = logs.filter(log => log.isAdjustment === true);
}
// ⏱️ Calculate work duration
for (const log of logs) {
const time = new Date(log.time);
if (log.loggedType === "LOGIN") {
lastLogin = time;
lastProjectId = log.projectId?._id?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
totalWorkSeconds += diffSec;
if (lastProjectId) {
projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec;
}
// reset after each pair
lastLogin = null;
lastProjectId = null;
} }
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 (already sorted by query, but ensure)
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?._id?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
if (lastProjectId) {
projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec;
}
// reset after each pair
lastLogin = null;
lastProjectId = null;
}
}
totalWorkSeconds += dayWorkSeconds;
} }
// 🕓 Convert total seconds to hh:mm:ss // 🕓 Convert total seconds to hh:mm:ss

View File

@ -60,13 +60,17 @@ async function va_timesheet_adjustment_function(req, res) {
const combineDateTime = (dateObj, timeStr) => { const combineDateTime = (dateObj, timeStr) => {
const [hours, minutes, seconds] = timeStr.split(':').map(Number); const [hours, minutes, seconds] = timeStr.split(':').map(Number);
// Extract date components from the Date object // // Extract date components from the Date object
const year = dateObj.getFullYear(); // const year = dateObj.getFullYear();
const month = dateObj.getMonth(); // const month = dateObj.getMonth();
const day = dateObj.getDate(); // const day = dateObj.getDate();
// Create UTC date directly // // Create UTC date directly
return new Date(Date.UTC(year, month, day, hours, minutes, seconds)); // return new Date(year, month, day, hours, minutes, seconds);
const combined = new Date(dateObj);
combined.setHours(hours, minutes, seconds, 0);
return combined;
}; };
const loginDateTime = combineDateTime(adjustmentDate, loginTime); const loginDateTime = combineDateTime(adjustmentDate, loginTime);

View File

@ -0,0 +1,290 @@
import { ClientModel, VAModel, ProjectModel } from "../models/vaModel.js";
/**
* Generic function to update relationships between Client, VA, and Project
* @param {Object} data - The update data object
* @returns {Object} - Result of the update operations
*/
async function updateEntityRelationships(data) {
try {
let results = {};
// Handle Project updates
if (data.project) {
const { projectId, vaId, clientId } = data.project;
if (!projectId) {
throw new Error('Project ID is required for project updates');
}
// Update Client with projectId
if (clientId) {
const clientUpdate = await ClientModel.findByIdAndUpdate(
clientId,
{
$addToSet: {
projectIds: projectId
}
},
{ new: true }
);
results.clientUpdate = clientUpdate ? 'Client updated successfully' : 'Client not found';
}
// Update VAs with projectId
if (vaId && Array.isArray(vaId) && vaId.length > 0) {
const vaUpdateResult = await VAModel.updateMany(
{ _id: { $in: vaId } },
{
$addToSet: {
projectIds: projectId
}
}
);
results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`;
}
// Update Project with clientId and vaId
const projectUpdateData = {};
if (clientId) {
projectUpdateData.clientId = clientId;
}
if (vaId && Array.isArray(vaId)) {
projectUpdateData.vaAssigned = vaId;
}
if (Object.keys(projectUpdateData).length > 0) {
const projectUpdate = await ProjectModel.findByIdAndUpdate(
projectId,
projectUpdateData,
{ new: true }
);
results.projectUpdate = projectUpdate ? 'Project updated successfully' : 'Project not found';
}
}
// Handle VA updates
if (data.va) {
const { vaId, projectId, clientId } = data.va;
if (!vaId) {
throw new Error('VA ID is required for VA updates');
}
// Update Projects with vaId
if (projectId && Array.isArray(projectId) && projectId.length > 0) {
const projectUpdateResult = await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{
$addToSet: {
vaAssigned: vaId
}
}
);
results.projectUpdate = `Updated ${projectUpdateResult.modifiedCount} projects`;
}
// Update Clients with vaId
if (clientId && Array.isArray(clientId) && clientId.length > 0) {
const clientUpdateResult = await ClientModel.updateMany(
{ _id: { $in: clientId } },
{
$addToSet: {
vaIds: vaId
}
}
);
results.clientUpdate = `Updated ${clientUpdateResult.modifiedCount} clients`;
}
// Update VA with clientId and projectId
const vaUpdateData = {};
if (clientId && Array.isArray(clientId)) {
vaUpdateData.clientIds = clientId;
}
if (projectId && Array.isArray(projectId)) {
vaUpdateData.projectIds = projectId;
}
if (Object.keys(vaUpdateData).length > 0) {
const vaUpdate = await VAModel.findByIdAndUpdate(
vaId,
{ $addToSet: vaUpdateData },
{ new: true }
);
results.vaUpdate = vaUpdate ? 'VA updated successfully' : 'VA not found';
}
}
// Handle Client updates
if (data.client) {
const { clientId, projectId, vaId } = data.client;
if (!clientId) {
throw new Error('Client ID is required for client updates');
}
// Update Projects with clientId
if (projectId && Array.isArray(projectId) && projectId.length > 0) {
const projectUpdateResult = await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{ clientId: clientId }
);
results.projectUpdate = `Updated ${projectUpdateResult.modifiedCount} projects`;
}
// Update VAs with clientId
if (vaId && Array.isArray(vaId) && vaId.length > 0) {
const vaUpdateResult = await VAModel.updateMany(
{ _id: { $in: vaId } },
{
$addToSet: {
clientIds: clientId
}
}
);
results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`;
}
// Update Client with projectId and vaId
const clientUpdateData = {};
if (projectId && Array.isArray(projectId)) {
clientUpdateData.projectIds = projectId;
}
if (vaId && Array.isArray(vaId)) {
clientUpdateData.vaIds = vaId;
}
if (Object.keys(clientUpdateData).length > 0) {
const clientUpdate = await ClientModel.findByIdAndUpdate(
clientId,
{ $addToSet: clientUpdateData },
{ new: true }
);
results.clientUpdate = clientUpdate ? 'Client updated successfully' : 'Client not found';
}
}
return {
success: true,
message: 'Relationships updated successfully',
results
};
} catch (error) {
console.error('Error updating entity relationships:', error);
return {
success: false,
message: error.message,
results: {}
};
}
}
/**
* Remove relationships (for deletion cases)
* @param {Object} data - The removal data object
* @returns {Object} - Result of the removal operations
*/
async function removeEntityRelationships(data) {
try {
let results = {};
// Handle Project removal
if (data.project) {
const { projectId, vaId, clientId } = data.project;
if (!projectId) {
throw new Error('Project ID is required for project removal');
}
// Remove projectId from Client
if (clientId) {
await ClientModel.findByIdAndUpdate(
clientId,
{ $pull: { projectIds: projectId } }
);
}
// Remove projectId from VAs
if (vaId && Array.isArray(vaId)) {
await VAModel.updateMany(
{ _id: { $in: vaId } },
{ $pull: { projectIds: projectId } }
);
}
results.projectRemoval = 'Project relationships removed successfully';
}
// Handle VA removal
if (data.va) {
const { vaId, projectId, clientId } = data.va;
if (!vaId) {
throw new Error('VA ID is required for VA removal');
}
// Remove vaId from Projects
if (projectId && Array.isArray(projectId)) {
await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{ $pull: { vaAssigned: vaId } }
);
}
// Remove vaId from Clients
if (clientId && Array.isArray(clientId)) {
await ClientModel.updateMany(
{ _id: { $in: clientId } },
{ $pull: { vaIds: vaId } }
);
}
results.vaRemoval = 'VA relationships removed successfully';
}
// Handle Client removal
if (data.client) {
const { clientId, projectId, vaId } = data.client;
if (!clientId) {
throw new Error('Client ID is required for client removal');
}
// Remove clientId from Projects
if (projectId && Array.isArray(projectId)) {
await ProjectModel.updateMany(
{ _id: { $in: projectId } },
{ clientId: null }
);
}
// Remove clientId from VAs
if (vaId && Array.isArray(vaId)) {
await VAModel.updateMany(
{ _id: { $in: vaId } },
{ $pull: { clientIds: clientId } }
);
}
results.clientRemoval = 'Client relationships removed successfully';
}
return {
success: true,
message: 'Relationships removed successfully',
results
};
} catch (error) {
console.error('Error removing entity relationships:', error);
return {
success: false,
message: error.message,
results: {}
};
}
}
export { updateEntityRelationships, removeEntityRelationships };

View File

@ -90,13 +90,14 @@ const projectSchema = new Schema({
ref: "VA", ref: "VA",
}, },
], ],
clientId: {
type: Types.ObjectId,
ref: "Client"
},
dedicatedHours: { dedicatedHours: {
type: Number, type: Number,
default: 0 default: 0
},
status: {
type: String,
enum: ["ACTIVE", "INACTIVE"],
default: "ACTIVE"
} }
}); });

View File

@ -9,6 +9,7 @@ import { client_refresh_token_callback } from "../../controllers/client/clientRe
import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js"; import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js";
import { clientAuthValidator } from "../../middlewares/auth.middleware.js"; import { clientAuthValidator } from "../../middlewares/auth.middleware.js";
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js"; import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js";
import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js"
// clients routes // clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/register", va_register_client); // Register a new VA
@ -22,7 +23,8 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback)
// dashboard API's // dashboard API's
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback (NC) clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA Clients Projects Callback

View File

@ -9,6 +9,9 @@ import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname } from 'path';
import { process_reset } from "../../controllers/passwordReset.controllers.js"; import { process_reset } from "../../controllers/passwordReset.controllers.js";
import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js"
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
import { authValidator } from "../../middlewares/auth.middleware.js";
const v1Routes = express.Router() const v1Routes = express.Router()
@ -24,6 +27,9 @@ v1Routes.use("/auth_callback", authRoutes);
v1Routes.use("/va", vaRoutes); v1Routes.use("/va", vaRoutes);
v1Routes.use("/client", clientRoutes); v1Routes.use("/client", clientRoutes);
v1Routes.post("/project", authValidator, project_add_sf);
v1Routes.patch("/project/:projectId", authValidator, update_project_sf);
v1Routes.post("/password/reset", process_reset); v1Routes.post("/password/reset", process_reset);
v1Routes.get("/", function(req, res) { v1Routes.get("/", function(req, res) {

View File

@ -53,11 +53,21 @@ app.use(bodyParser.text());
// API Route // API Route
app.use('/api', apiRoutes); app.use('/api', apiRoutes);
app.get('/varegister', (req, res) => { app.get('/varegister/:sf_token?', (req, res) => {
// const { sf_token } = useParams();
res.render('va_register', { title: "VA Registration" }); res.render('va_register', { title: "VA Registration" });
// if(verified) {
// res.render('va_register', { title: "VA Registration" });
// } else {
// res.render('not_founf', { title: "VA Registration" });
// }
}); });
app.get('/clientregister', (req, res) => { app.get('/clientregister/:sf_token?', (req, res) => {
// const { sf_token } = useParams();
res.render('va_client_register', { title: "Client Registration" }); res.render('va_client_register', { title: "Client Registration" });
}); });

View File

@ -148,7 +148,8 @@
<th>Status</th> <th>Status</th>
<th>Virtual Assistant</th> <th>Virtual Assistant</th>
<th>Projects Assigned</th> <th>Projects Assigned</th>
<th>Worked Time</th> <th>Working from</th>
<th>Total Worked</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
@ -221,7 +222,7 @@
// Format time function // Format time function
function formatTime(seconds) { function formatTime(seconds) {
if (!seconds) return 'Not working'; if (!seconds) return '';
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
@ -230,6 +231,8 @@
return `${hours}h ${minutes}m`; return `${hours}h ${minutes}m`;
} else if (minutes > 0) { } else if (minutes > 0) {
return `${minutes}m`; return `${minutes}m`;
} else if (seconds > 0) {
return `${seconds}s`;
} else { } else {
return 'Just started'; return 'Just started';
} }
@ -240,6 +243,7 @@
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const totalWorkedTime = formatTime(va.totalWorkedSeconds);
const projectsAssigned = va.projectsAssigned || 0; const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank"> const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
@ -247,7 +251,7 @@
</a>` : ''; </a>` : '';
return ` return `
<tr> <tr data-vaid="${va.id}">
<td> <td>
<div class="status-indicator"> <div class="status-indicator">
<div class="status-dot ${statusClass}"></div> <div class="status-dot ${statusClass}"></div>
@ -256,7 +260,7 @@
</td> </td>
<td> <td>
<div class="va-info"> <div class="va-info">
<div class="va-name">${va.firstName} ${va.lastName}</div> <div class="va-name">${va.name}</div>
<div class="va-email">${va.email || 'N/A'}</div> <div class="va-email">${va.email || 'N/A'}</div>
</div> </div>
</td> </td>
@ -266,6 +270,9 @@
<td> <td>
<span class="worked-time">${workedTime}</span> <span class="worked-time">${workedTime}</span>
</td> </td>
<td>
<span class="worked-time">${totalWorkedTime}</span>
</td>
<td> <td>
${viewLink} ${viewLink}
</td> </td>
@ -303,15 +310,34 @@
vaTableBody.style.display = ''; vaTableBody.style.display = '';
vaEmptyState.style.display = 'none'; vaEmptyState.style.display = 'none';
// Add event listeners to view links // Add event listeners to table rows
document.querySelectorAll('.view-link').forEach(link => { document.querySelectorAll('tr[data-vaid]').forEach(row => {
link.addEventListener('click', function(e) { row.addEventListener('click', async function(e) {
e.preventDefault(); // Don't trigger if click is on view link
const vaId = this.getAttribute('data-va-id'); if (e.target.closest('.view-link')) {
// You can update this link as needed return;
window.location.href = `/client/virtual-assistants/${vaId}`; }
const vaId = this.getAttribute('data-vaid');
const vaData = allVAs.find(va => va.id === vaId);
if (vaData) {
// Show loading state
const originalContent = this.innerHTML;
this.innerHTML = '<td colspan="6" style="text-align: center;"><i class="fas fa-spinner fa-spin"></i> Loading...</td>';
// Fetch additional VA details from /test API
const projectData = await fetchVADetails(vaId);
// Restore original content
this.innerHTML = originalContent;
// Show popup with VA details
showVAPopup(projectData);
}
}); });
}); });
} else { } else {
vaTableBody.innerHTML = ''; vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none'; vaTableBody.style.display = 'none';
@ -351,6 +377,127 @@
// Optional: Auto-refresh data every 30 seconds // Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000); setInterval(loadDashboardData, 30000);
function createPopup(vaData) {
// Extract project details from the API response
// The API response is the project array, so use it directly
const projectDetails = Array.isArray(vaData) ? vaData : (vaData.projects || []);
// Create project details HTML
const projectsHTML = projectDetails.length > 0
? projectDetails.map(project => `
<div class="project-item">
<div class="project-header">
<span class="project-name">${project.projectName || 'Unknown Project'}</span>
<span class="project-time">${project.totalTime || '00:00:00'}</span>
</div>
<div class="project-id">ID: ${project.projectId || 'N/A'}</div>
</div>
`).join('')
: '<div class="no-projects">No project data available</div>';
return `
<div class="popup-overlay" id="vaPopup">
<div class="popup-content">
<div class="popup-header">
<h3>Project Work Details</h3>
<button class="popup-close" id="closePopup">
<i class="fas fa-times"></i>
</button>
</div>
<div class="popup-body">
<!-- Project Count -->
<div class="project-summary">
<div class="total-projects">
<i class="fas fa-folder-open"></i>
<span>Total Projects: ${projectDetails.length}</span>
</div>
</div>
<!-- Project Details List -->
<div class="projects-list">
${projectsHTML}
</div>
</div>
<div class="popup-footer">
<button class="btn btn-secondary" id="closePopupBtn">
<i class="fas fa-times"></i> Close
</button>
</div>
</div>
</div>
`;
}
// Show popup with VA details
function showVAPopup(projectData) {
console.log('Project Data:', projectData);
// Remove existing popup if any
const existingPopup = document.getElementById('vaPopup');
if (existingPopup) {
existingPopup.remove();
}
// Create and show new popup with only project data
const popupHTML = createPopup(projectData);
document.body.insertAdjacentHTML('beforeend', popupHTML);
// Add event listeners for closing popup
document.getElementById('closePopup').addEventListener('click', closePopup);
document.getElementById('closePopupBtn').addEventListener('click', closePopup);
// Close popup when clicking outside
document.getElementById('vaPopup').addEventListener('click', function(e) {
if (e.target === this) {
closePopup();
}
});
// Close popup with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePopup();
}
});
}
// Fetch VA details for popup - now returns only project data
async function fetchVADetails(vaId) {
try {
const response = await fetchWithAuth(`${API_BASE}/dashboard/va-report`, {
method: 'POST',
body: JSON.stringify({ vaId: vaId }),
headers: {
'Content-Type': 'application/json'
}
});
if (!response) return;
const result = await response.json();
console.log('API Response:', result);
if (result.success) {
// Return the projects array directly
return result.projects || [];
} else {
console.error('Failed to fetch project details:', result.message);
return [];
}
} catch (error) {
console.error('Error fetching project details:', error);
return [];
}
}
// Close popup function
function closePopup() {
const popup = document.getElementById('vaPopup');
if (popup) {
popup.remove();
}
}
</script> </script>
</body> </body>

View File

@ -96,47 +96,27 @@
</aside> </aside>
<main class="dashboard-main"> <main class="dashboard-main">
<div class="dashboard-content"> <div class="dashboard-content">
<div class="page-header">
<h1 class="page-title">Virtual Assistants</h1>
<a href="/client/virtual-assistants/tips" class="action-button">
<i class="fas fa-gift"></i>Manage Tips
</a>
</div>
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-info"> <div class="stat-info">
<h3>Active VAs</h3> <h3>Active VA</h3>
<div class="stat-value" id="activeVaCount">0</div> <div class="stat-value" id="activeVaCount">0</div>
</div> </div>
<div class="stat-icon blue"> <div class="stat-icon blue">
<i class="fas fa-users"></i> <i class="fas fa-users fa-xl"></i>
</div> </div>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-info"> <div class="stat-info">
<h3>Unbilled Hours</h3> <h3>Total VA's</h3>
<div class="stat-value" id="unbilledHours">0</div> <div class="stat-value" id="totalVaCount">0</div>
</div>
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Billed Hours</h3>
<div class="stat-value" id="billedHours">0</div>
</div> </div>
<div class="stat-icon blue"> <div class="stat-icon blue">
<i class="fas fa-clock"></i> <i class="fas fa-users fa-xl"></i>
</div> </div>
</div> </div>
</div> </div>
@ -144,143 +124,234 @@
<div class="stat-card"> <div class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-info"> <div class="stat-info">
<h3>Billed Amount</h3> <h3>Total Projects</h3>
<div class="stat-value" id="billedAmount">$0.00</div> <div class="stat-value" id="totalProjects">0</div>
</div> </div>
<div class="stat-icon yellow"> <div class="stat-icon green">
<i class="fas fa-dollar-sign"></i> <i class="fas fa-clock fa-xl"></i>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Virtual Assistants Table --> <!-- Virtual Assistants List -->
<div class="table-container"> <div class="content-section">
<div class="table-wrapper"> <div class="section-header">
<table class="data-table"> <h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
<thead>
<tr> </div>
<th>Status</th> <div class="table-container">
<th>Virtual Assistant</th> <div class="table-wrapper">
<th>Project</th> <table class="data-table">
<th>Unbilled Hours</th> <thead>
<th>Billed Hours</th> <tr>
<th>Billed Tips</th> <th>Status</th>
<th>Billed Amount</th> <th>Virtual Assistant</th>
</tr> <th>Projects Assigned</th>
</thead> <th>Worked Time</th>
<tbody id="vaTableBody"> <th>Action</th>
<!-- Data will be loaded here --> </tr>
</tbody> </thead>
</table> <tbody id="vaTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div id="vaEmptyState" style="display: none;">
<div class="empty-state">
<i class="fas fa-users"></i>
<h3>No Virtual Assistants</h3>
<p>You haven't assigned any virtual assistants yet.</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</div> </div>
</div> </div>
<script> <script>
async function loadVirtualAssistants() { // Theme management
try { function setTheme(theme) {
const response = await fetchWithAuth(`${API_BASE}/virtual-assistants`); document.documentElement.setAttribute('data-theme', theme);
if (!response) return; localStorage.setItem('theme', theme);
}
const data = await response.json(); function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Update stats // Initialize theme
document.getElementById('activeVaCount').textContent = data.stats?.activeVaCount || 0; const savedTheme = localStorage.getItem('theme') || getSystemTheme();
document.getElementById('unbilledHours').textContent = data.stats?.unbilledHours || '0'; setTheme(savedTheme);
document.getElementById('billedHours').textContent = data.stats?.billedHours || '0';
document.getElementById('billedAmount').textContent = `$${data.stats?.billedAmount || '0.00'}`;
// Update table // API base URL
const tableBody = document.getElementById('vaTableBody'); const API_BASE = '/api/v1/client';
if (data.virtualAssistants && data.virtualAssistants.length > 0) {
tableBody.innerHTML = data.virtualAssistants.map(va => ` // Auth token management
<tr> function getAuthToken() {
<td> return localStorage.getItem('knoptpq');
<span class="status-badge status-active">${va.status}</span> }
</td>
<td>${va.name}</td> async function fetchWithAuth(url, options = {}) {
<td>${va.project}</td> const token = getAuthToken();
<td> if (!token) {
<span class="hours-badge hours-default"> window.location.href = '/client/login';
${va.unbilledHours}<span class="hours-unit">hrs</span> return;
</span> }
</td>
<td> const response = await fetch(url, {
<span class="hours-badge hours-primary"> ...options,
${va.billedHours || '-'}<span class="hours-unit">hr</span> headers: {
</span> 'token': `${token}`,
</td> 'Content-Type': 'application/json',
<td class="amount-tips">$${va.billedTips || '0.00'}</td> ...options.headers,
<td class="amount-positive">$${va.billedAmount || '0.00'}</td> }
</tr> });
`).join('');
if (response.status === 401) {
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
return;
}
return response;
}
// Format time function
function formatTime(seconds) {
if (!seconds) return 'Not working';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else if (minutes > 0) {
return `${minutes}m`;
} else {
return 'Just started';
}
}
// Create VA card HTML
function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
<i class="fas fa-eye"></i>
View
</a>` : '';
return `
<tr>
<td>
<div class="status-indicator">
<div class="status-dot ${statusClass}"></div>
<span class="status-text ${statusClass}">${statusText}</span>
</div>
</td>
<td>
<div class="va-info">
<div class="va-name">${va.name}</div>
<div class="va-email">${va.email || 'N/A'}</div>
</div>
</td>
<td>
<span class="projects-count">${projectsAssigned} Projects</span>
</td>
<td>
<span class="worked-time">${workedTime}</span>
</td>
<td>
${viewLink}
</td>
</tr>
`;
}
// Load dashboard data
async function loadDashboardData() {
try {
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
if (!response) return;
const result = await response.json();
// console.log(data)
if (result.success) {
const data = result.data;
const summary = result.summary;
// Update stats cards
document.getElementById('activeVaCount').textContent = summary.activeVAs || 0;
document.getElementById('totalVaCount').textContent = summary.totalVAs || 0;
document.getElementById('totalProjects').textContent = summary.totalProjects || 0;
// Update VA list count
document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`;
// Update VA table
const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState');
const allVAs = data.allVAs?.total || [];
if (allVAs.length > 0) {
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = '';
vaEmptyState.style.display = 'none';
// Add event listeners to view links
document.querySelectorAll('.view-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const vaId = this.getAttribute('data-va-id');
// You can update this link as needed
window.location.href = `/client/virtual-assistants/${vaId}`;
});
});
} else { } else {
tableBody.innerHTML = ` vaTableBody.innerHTML = '';
<tr> vaTableBody.style.display = 'none';
<td colspan="7" class="empty-state">No virtual assistants found</td> vaEmptyState.style.display = 'block';
</tr>
`;
} }
} catch (error) { } else {
console.error('Error loading virtual assistants:', error); console.error('Failed to load dashboard data:', result.message);
showError('Failed to load dashboard data');
} }
} catch (error) {
console.error('Error loading dashboard data:', error);
showError('Error loading dashboard data');
} }
}
document.addEventListener('DOMContentLoaded', loadVirtualAssistants); // Show error message
</script> function showError(message) {
const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState');
vaTableBody.style.display = 'none';
vaEmptyState.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-triangle"></i>
<h3>Error</h3>
<p>${message}</p>
</div>
`;
vaEmptyState.style.display = 'block';
}
// Load data when page loads
document.addEventListener('DOMContentLoaded', loadDashboardData);
// Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
<script> </script>
// Theme management
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Initialize theme
const savedTheme = localStorage.getItem('theme') || getSystemTheme();
setTheme(savedTheme);
// // API base URL
// const API_BASE = '/api/v1/client';
// // Auth token management
// function getAuthToken() {
// return localStorage.getItem('authToken');
// }
// async function fetchWithAuth(url, options = {}) {
// const token = getAuthToken();
// if (!token) {
// window.location.href = '/client/login';
// return;
// }
// const response = await fetch(url, {
// ...options,
// headers: {
// 'Authorization': `Bearer ${token}`,
// 'Content-Type': 'application/json',
// ...options.headers,
// }
// });
// if (response.status === 401) {
// localStorage.removeItem('authToken');
// window.location.href = '/client/login';
// return;
// }
// return response;
// }
</script>
</body> </body>
</html> </html>