updated dashboard api

This commit is contained in:
Aakash Mittal 2025-11-17 15:57:39 +05:30
parent f9b8cd886a
commit f2f8319565
8 changed files with 638 additions and 190 deletions

View File

@ -286,6 +286,10 @@ body {
}
}
.section-title {
margin-bottom: 20px;
}
.stat-card {
background-color: var(--bg-card);
border-radius: 0.75rem;
@ -354,6 +358,13 @@ body {
gap: 1.5rem;
}
.status-indicator:has(>.active) {
text-align: center;
padding: 4px;
background-color: rgba(51, 255, 0, 0.24);
border-radius: 20px;
}
@media (min-width: 1024px) {
.content-grid {
grid-template-columns: repeat(2, 1fr);

View File

@ -0,0 +1,311 @@
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 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 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
};
}
}
return {
id: v._id,
firstName: v.userDetails?.firstName || '',
lastName: v.userDetails?.lastName || '',
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,
totalLogins: activity.totalLogins || 0,
lastProject: currentProjectDetails, // Now with project details
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 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
};
}
}
return {
id: v._id,
firstName: v.userDetails?.firstName || '',
lastName: v.userDetails?.lastName || '',
email: v.userDetails?.email,
phone: v.userDetails?.phone,
status: v.status || 'active',
lastLoginTime: lastLog?.time || null,
currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted,
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 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,
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
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
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 };

View File

@ -46,6 +46,55 @@ async function authValidator(req, res, next) {
}
}
export {
authValidator
async function clientAuthValidator(req, res, next) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (decoded && decoded.id && decoded.email) {
const clientEmail = decoded.email;
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
if (!clientUser) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (!clientUser.accessToken || clientUser.accessToken !== accessToken) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
// Attach client info to request object for use in subsequent middleware/controllers
req.body.clientId = clientUser._id;
req.body.clientEmail = clientEmail;
req.body.clientName = decoded.name;
next();
} else {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
} catch (error) {
logger.error("clientAuthValidator - Middleware (CATCH Block)", error.message);
if (error.name === "TokenExpiredError") {
return res.status(401).json({ success: false, message: "Token expired" });
}
if (error.name === "JsonWebTokenError") {
return res.status(401).json({ success: false, message: "Invalid token" });
}
return res.status(401).json({ success: false, message: "Authentication failed" });
}
}
export {
authValidator,
clientAuthValidator
}

View File

@ -2,11 +2,13 @@ import express from "express";
const clientRoutes = express.Router();
import { va_register_client } from "../../controllers/client/vaClient.controller.js";
import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
// import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.controller.js";
import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js";
import { clientAuthValidator } from "../../middlewares/auth.middleware.js";
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js";
// clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA
@ -14,11 +16,16 @@ clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.get("/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC)
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
// dashboard API's
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback (NC)
export default clientRoutes;

View File

@ -63,8 +63,9 @@ app.get("/client/forgot-password", (req, res) => {
// Client Dashboard Routes with new file names
app.get('/client/dashboard', async (req, res) => {
try {
const dashboardData = await dashboardService.getClientDashboard(req.user.id);
res.render('client/dashboard-dashboard', dashboardData);
// const dashboardData = await dashboardService.getClientDashboard(req.user.id);
// res.render('client/dashboard-dashboard', dashboardData);
res.render('client/dashboard-dashboard');
} catch (error) {
console.error('Dashboard error:', error);
res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' });
@ -73,8 +74,9 @@ app.get('/client/dashboard', async (req, res) => {
app.get('/client/virtual-assistants', async (req, res) => {
try {
const vaData = await virtualAssistantService.getClientVAs(req.user.id);
res.render('client/dashboard-virtual-assistants', vaData);
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
res.render('client/dashboard-virtual-assistants');
// res.render('client/dashboard-virtual-assistants', vaData);
} catch (error) {
console.error('Virtual assistants error:', error);
res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' });
@ -111,7 +113,7 @@ app.get('/client/virtual-assistants', async (req, res) => {
// }
// });
app.get("/live/:projectId", (req, res) => {
app.get("/live/:projectId/:vaId", (req, res) => {
const { projectId } = req.params;
res.render("va_live_tracker", { title: "VA Live Tracker", projectId })
})

View File

@ -24,16 +24,16 @@
<i class="fas fa-chart-bar"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a href="/client/invoices" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices
</a>
</li>
<li class="nav-item">
<a href="/client/virtual-assistants" class="nav-button">
<i class="fas fa-users"></i>Virtual Assistants
</a>
</li>
<!-- <li class="nav-item">
<a href="/client/invoices" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices
</a>
</li>
<li class="nav-item">
<a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets
@ -43,7 +43,7 @@
<a href="/client/referrals" class="nav-button">
<i class="fas fa-share-alt"></i>Referrals
</a>
</li>
</li> -->
</ul>
<div class="user-avatar">TT</div>
@ -73,7 +73,7 @@
<span>Virtual Assistants</span>
</a>
</li>
<li class="sidebar-item">
<!-- <li class="sidebar-item">
<a href="/client/timesheets" class="sidebar-link">
<i class="fas fa-clock sidebar-icon"></i>
<span>Timesheets</span>
@ -90,7 +90,7 @@
<i class="fas fa-share-alt sidebar-icon"></i>
<span>Referrals</span>
</a>
</li>
</li> -->
</ul>
</nav>
</aside>
@ -101,7 +101,7 @@
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Active Virtual Assistants</h3>
<h3>Active VA</h3>
<div class="stat-value" id="activeVaCount">0</div>
</div>
<div class="stat-icon blue">
@ -109,184 +109,249 @@
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Total VA's</h3>
<div class="stat-value" id="totalVaCount">0</div>
</div>
<div class="stat-icon blue">
<i class="fas fa-users fa-xl"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Total Billed Hours</h3>
<div class="stat-value" id="totalBilledHours">0</div>
<h3>Total Projects</h3>
<div class="stat-value" id="totalProjects">0</div>
</div>
<div class="stat-icon green">
<i class="fas fa-clock fa-xl"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Unpaid Invoices</h3>
<div class="stat-value" id="unpaidInvoices">$0.00</div>
</div>
<div class="stat-icon red">
<i class="fas fa-file-invoice fa-xl"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Outstanding Balance</h3>
<div class="stat-value" id="outstandingBalance">$0.00</div>
</div>
<div class="stat-icon yellow">
<i class="fas fa-money-bill-wave fa-xl"></i>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="content-grid">
<!-- Recent Timesheets -->
<div class="content-card">
<div class="card-header">
<h3 class="card-title">Recent Timesheets</h3>
<a href="/client/timesheets" class="card-link">View All</a>
</div>
<div class="activity-list" id="recentTimesheets">
<!-- Timesheets will be loaded here -->
</div>
</div>
<!-- Virtual Assistants List -->
<div class="content-section">
<div class="section-header">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
<!-- Recent Invoices -->
<div class="content-card">
<div class="card-header">
<h3 class="card-title">Recent Invoices</h3>
<a href="/client/invoices" class="card-link">View All</a>
</div>
<div class="table-container">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Status</th>
<th>Virtual Assistant</th>
<th>Projects Assigned</th>
<th>Worked Time</th>
<th>Action</th>
</tr>
</thead>
<tbody id="vaTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div class="activity-list" id="recentInvoices">
<!-- Invoices will be loaded here -->
<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>
</main>
</main>
</div>
</div>
<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('knoptpq');
}
async function fetchWithAuth(url, options = {}) {
const token = getAuthToken();
if (!token) {
window.location.href = '/client/login';
return;
}
const response = await fetch(url, {
...options,
headers: {
'token': `${token}`,
'Content-Type': 'application/json',
...options.headers,
}
});
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.firstName} ${va.lastName}</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)
const data = await response.json();
if (result.success) {
const data = result.data;
const summary = result.summary;
// Update stats
document.getElementById('activeVaCount').textContent = data.activeVaCount || 0;
document.getElementById('totalBilledHours').textContent = data.totalBilledHours || '0';
document.getElementById('unpaidInvoices').textContent = `$${data.unpaidInvoices || '0.00'}`;
document.getElementById('outstandingBalance').textContent = `$${data.outstandingBalance || '0.00'}`;
// 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 {
vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none';
vaEmptyState.style.display = 'block';
}
// Update recent timesheets
const timesheetsContainer = document.getElementById('recentTimesheets');
if (data.recentTimesheets && data.recentTimesheets.length > 0) {
timesheetsContainer.innerHTML = data.recentTimesheets.map(timesheet => `
<div class="activity-item">
<div class="activity-info">
<h4>${timesheet.virtualAssistant}</h4>
<p>${timesheet.project}</p>
</div>
<div class="activity-meta">
<div class="value">${timesheet.hours}h</div>
<div class="date">${timesheet.date}</div>
</div>
</div>
`).join('');
} else {
timesheetsContainer.innerHTML = '<div class="empty-state">No recent timesheets</div>';
}
// Update recent invoices
const invoicesContainer = document.getElementById('recentInvoices');
if (data.recentInvoices && data.recentInvoices.length > 0) {
invoicesContainer.innerHTML = data.recentInvoices.map(invoice => `
<div class="activity-item">
<div class="activity-info">
<h4>Invoice #${invoice.id}</h4>
<p>Due: ${invoice.dueDate}</p>
</div>
<div class="activity-meta">
<div class="value">$${invoice.amount}</div>
<span class="status-badge ${invoice.status === 'paid' ? 'status-paid' : 'status-unpaid'}">
${invoice.status}
</span>
</div>
</div>
`).join('');
} else {
invoicesContainer.innerHTML = '<div class="empty-state">No recent invoices</div>';
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');
}
}
// Show error message
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>
</html>

View File

@ -29,7 +29,7 @@
<i class="fas fa-users"></i>Virtual Assistants
</a>
</li>
<li class="nav-item">
<!-- <li class="nav-item">
<a href="/client/invoices" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices
</a>
@ -43,7 +43,7 @@
<a href="/client/referrals" class="nav-button">
<i class="fas fa-share-alt"></i>Referrals
</a>
</li>
</li> -->
</ul>
<div class="user-avatar">TT</div>
@ -73,7 +73,7 @@
<span>Virtual Assistants</span>
</a>
</li>
<li class="sidebar-item">
<!-- <li class="sidebar-item">
<a href="/client/timesheets" class="sidebar-link">
<i class="fas fa-clock sidebar-icon"></i>
<span>Timesheets</span>
@ -90,7 +90,7 @@
<i class="fas fa-share-alt sidebar-icon"></i>
<span>Referrals</span>
</a>
</li>
</li> -->
</ul>
</nav>
</aside>

View File

@ -298,35 +298,38 @@
if (!authToken) {
return false; // No token found
} else {
window.location.href = "/client/dashboard";
return true;
}
try {
// Validate the token with the server
const response = await fetch("/api/v1/client/validate-token", {
method: "GET",
headers: {
"Authorization": `Bearer ${authToken}`,
"Content-Type": "application/json"
}
});
// try {
// // Validate the token with the server
// const response = await fetch("/api/v1/client/validate-token", {
// method: "GET",
// headers: {
// "Authorization": `Bearer ${authToken}`,
// "Content-Type": "application/json"
// }
// });
if (response.ok) {
// Token is valid, redirect to dashboard
window.location.href = "/client/dashboard";
return true;
} else {
// Token is invalid, clear localStorage
localStorage.removeItem("knoptpq");
localStorage.removeItem("refreshToken");
return false;
}
} catch (error) {
console.error("Token validation error:", error);
// If validation fails, clear tokens to be safe
localStorage.removeItem("knoptpq");
localStorage.removeItem("refreshToken");
return false;
}
// if (response.ok) {
// // Token is valid, redirect to dashboard
// window.location.href = "/client/dashboard";
// return true;
// } else {
// // Token is invalid, clear localStorage
// localStorage.removeItem("knoptpq");
// localStorage.removeItem("refreshToken");
// return false;
// }
// } catch (error) {
// console.error("Token validation error:", error);
// // If validation fails, clear tokens to be safe
// localStorage.removeItem("knoptpq");
// localStorage.removeItem("refreshToken");
// return false;
// }
}
</script>
</body>