518 lines
20 KiB
Plaintext
518 lines
20 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en" data-theme="light">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>DocVA Client | Dashboard</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
|
</head>
|
|
|
|
<body class="dashboard-body">
|
|
<div id="root">
|
|
<nav class="dashboard-header">
|
|
<div class="header-content">
|
|
<div class="header-logo">
|
|
<div class="logo-container">
|
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header-nav">
|
|
<ul class="nav-list">
|
|
<li class="nav-item">
|
|
<a href="/client/dashboard" class="nav-button">
|
|
<i class="fas fa-chart-bar"></i>Dashboard
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/client/virtual-assistants" class="nav-button active">
|
|
<i class="fas fa-users"></i>Virtual Assistants
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/client/projects" class="nav-button">
|
|
<i class="fas fa-file-invoice"></i>Projects
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<button class="mobile-menu-button">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div class="dashboard-container">
|
|
<aside class="dashboard-sidebar">
|
|
<nav class="sidebar-nav">
|
|
<ul class="sidebar-list">
|
|
<li class="sidebar-item">
|
|
<a href="/client/profile" class="sidebar-link">
|
|
<i class="fas fa-user sidebar-icon"></i>
|
|
<span>Profile</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="/client/dashboard" class="sidebar-link">
|
|
<i class="fas fa-chart-bar sidebar-icon"></i>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="/client/virtual-assistants" class="sidebar-link active">
|
|
<i class="fas fa-users sidebar-icon"></i>
|
|
<span>Virtual Assistants</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="/client/projects" class="sidebar-link">
|
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
|
<span>Projects</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
|
|
<i class="fas fa-sign-out-alt sidebar-icon"></i>
|
|
<span>Logout</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
<main class="dashboard-main">
|
|
<div class="dashboard-content">
|
|
<!-- Virtual Assistants List -->
|
|
<div class="content-section">
|
|
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
|
<a href="/client/virtual-assistants" class="btn btn-secondary">
|
|
<i class="fas fa-arrow-left"></i> Back to VAs
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loadingState" class="loading-spinner">
|
|
<div class="spinner"></div>
|
|
<p>Loading VA logs...</p>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="emptyState" class="empty-state" style="display: none;">
|
|
<i class="fas fa-clipboard-list"></i>
|
|
<h3>No Logs Found</h3>
|
|
<p>This virtual assistant hasn't logged any time in the last 15 days.</p>
|
|
</div>
|
|
|
|
<!-- Projects Accordion Container -->
|
|
<div id="projectsContainer" style="display: none;"></div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<div id="logoutModal" class="modal-overlay">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">Confirm Logout</h2>
|
|
<button class="modal-close" onclick="closeLogoutModal()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="logout-modal-body">
|
|
<div class="logout-icon">
|
|
<i class="fas fa-sign-out-alt"></i>
|
|
</div>
|
|
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
|
<p class="logout-message">You will be logged out of your account and redirected to the login page.
|
|
</p>
|
|
<div class="logout-actions">
|
|
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
|
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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 = 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 = '/';
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'token': `${token}`,
|
|
'type': 'client',
|
|
'Content-Type': 'application/json',
|
|
'ngrok-skip-browser-warning': 69420,
|
|
...options.headers,
|
|
}
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
localStorage.removeItem('knoptpq');
|
|
localStorage.removeItem('refreshToken');
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
// Call logout API
|
|
await fetchWithAuth(`${API_BASE}/logout`, {
|
|
method: 'POST'
|
|
});
|
|
} catch (error) {
|
|
console.error('Logout API error:', error);
|
|
} finally {
|
|
// Clear local storage and redirect
|
|
localStorage.removeItem('knoptpq');
|
|
localStorage.removeItem('refreshToken');
|
|
window.location.href = '/';
|
|
}
|
|
}
|
|
|
|
// Logout Modal Functions
|
|
function openLogoutModal() {
|
|
const modal = document.getElementById('logoutModal');
|
|
modal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeLogoutModal() {
|
|
const modal = document.getElementById('logoutModal');
|
|
modal.style.display = 'none';
|
|
document.body.style.overflow = 'auto';
|
|
}
|
|
|
|
function toggleAccordion(accordionId) {
|
|
const accordion = document.getElementById(accordionId);
|
|
accordion.classList.toggle('active');
|
|
|
|
// const allAccordions = document.querySelectorAll('.accordion');
|
|
// allAccordions.forEach(acc => {
|
|
// if (acc.id !== accordionId && acc.classList.contains('active')) {
|
|
// acc.classList.remove('active');
|
|
// }
|
|
// });
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
if (!seconds) return '0m';
|
|
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
return `${minutes}m`;
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
function formatTimeOnly(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
});
|
|
}
|
|
|
|
function calculateTotalIndividualLogsHours(workTrackLogs) {
|
|
let total = 0;
|
|
workTrackLogs.forEach(workTrack => {
|
|
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
|
workTrack.logs.forEach(log => {
|
|
total += log.totalTime || 0;
|
|
});
|
|
}
|
|
});
|
|
return total;
|
|
}
|
|
|
|
function countIndividualLogs(workTrackLogs) {
|
|
let count = 0;
|
|
workTrackLogs.forEach(workTrack => {
|
|
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
|
count += workTrack.logs.length;
|
|
}
|
|
});
|
|
return count;
|
|
}
|
|
|
|
function calculateTotalHours(logs) {
|
|
return logs.reduce((total, log) => total + (log.totalTime || 0), 0);
|
|
}
|
|
|
|
// Create project accordion
|
|
function createProjectAccordion(projectData, index) {
|
|
const workTrackLogs = projectData.workTrackLogs || [];
|
|
const totalHours = calculateTotalIndividualLogsHours(workTrackLogs);
|
|
const individualLogsCount = countIndividualLogs(workTrackLogs);
|
|
const projectName = projectData.projectDetails?.name || `Project ${index + 1}`;
|
|
|
|
const accordionId = `project-${index}`;
|
|
|
|
return `
|
|
<div class="accordion" id="${accordionId}">
|
|
<div class="accordion-header" onclick="toggleAccordion('${accordionId}')">
|
|
<div class="accordion-title">
|
|
${projectName}
|
|
<span class="logs-count">${individualLogsCount} activities</span>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
<div style="text-align: right;">
|
|
<div class="time-badge">${formatTime(totalHours)}</div>
|
|
</div>
|
|
<i class="fas fa-chevron-down accordion-icon"></i>
|
|
</div>
|
|
</div>
|
|
<div class="accordion-content">
|
|
${individualLogsCount > 0 ? createIndividualLogsTable(workTrackLogs) : createNoLogsMessage()}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Create table for individual log entries
|
|
function createIndividualLogsTable(workTrackLogs) {
|
|
// Collect all individual logs from all work tracks
|
|
const allIndividualLogs = [];
|
|
workTrackLogs.forEach(workTrack => {
|
|
if (workTrack.logs && Array.isArray(workTrack.logs)) {
|
|
workTrack.logs.forEach(log => {
|
|
allIndividualLogs.push(log);
|
|
});
|
|
}
|
|
});
|
|
|
|
return `
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Title</th>
|
|
<th>App Name</th>
|
|
<th>From</th>
|
|
<th>To</th>
|
|
<th>Total Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${allIndividualLogs.map((log, index) => createIndividualLogRow(log, index)).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Create a single row for individual log entry
|
|
function createIndividualLogRow(log, index) {
|
|
const title = log.title || `Activity ${index + 1}`;
|
|
const appName = log.appName || 'Unknown App';
|
|
|
|
// Parse dates
|
|
const fromDate = log.from;
|
|
const toDate = log.to || log.from;
|
|
|
|
// Calculate duration
|
|
let duration = log.totalTime || 0;
|
|
if (!duration && fromDate && toDate) {
|
|
const from = new Date(fromDate);
|
|
const to = new Date(toDate);
|
|
duration = Math.floor((to - from) / 1000);
|
|
}
|
|
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<strong>${title}</strong>
|
|
</td>
|
|
<td>${appName}</td>
|
|
<td>
|
|
<div class="datetime-cell">
|
|
<div class="date-text">${formatDate(fromDate)}</div>
|
|
<div class="time-text">${formatTimeOnly(fromDate)}</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="datetime-cell">
|
|
<div class="date-text">${formatDate(toDate)}</div>
|
|
<div class="time-text">${formatTimeOnly(toDate)}</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="time-badge">${formatTime(duration)}</span>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// Create no logs message
|
|
function createNoLogsMessage() {
|
|
return `
|
|
<div style="text-align: center; padding: 2rem; color: #64748b;">
|
|
<i class="fas fa-clipboard"></i>
|
|
<h4>No Activity Logs</h4>
|
|
<p>No detailed activity logs available for this project.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Load VA logs
|
|
async function loadVALogs() {
|
|
const loadingState = document.getElementById('loadingState');
|
|
const emptyState = document.getElementById('emptyState');
|
|
const projectsContainer = document.getElementById('projectsContainer');
|
|
|
|
try {
|
|
// Get VA ID from URL
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const vaId = urlParams.get('va');
|
|
|
|
if (!vaId) {
|
|
window.location.href = '/client/virtual-assistants';
|
|
return;
|
|
}
|
|
|
|
// Fetch VA logs data
|
|
const response = await fetchWithAuth(`${API_BASE}/vaLogsData/${vaId}`);
|
|
|
|
if (!response || !response.ok) {
|
|
throw new Error('Failed to load VA logs');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.message || 'Failed to load VA logs');
|
|
}
|
|
|
|
// Hide loading state
|
|
loadingState.style.display = 'none';
|
|
|
|
const projectLogs = data.data?.projectLogs || [];
|
|
|
|
// Handle empty state
|
|
if (projectLogs.length === 0) {
|
|
emptyState.style.display = 'block';
|
|
projectsContainer.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Show projects container
|
|
projectsContainer.style.display = 'block';
|
|
emptyState.style.display = 'none';
|
|
|
|
// Create accordions for each project
|
|
let accordionsHTML = '';
|
|
projectLogs.forEach((project, index) => {
|
|
accordionsHTML += createProjectAccordion(project, index);
|
|
});
|
|
|
|
projectsContainer.innerHTML = accordionsHTML;
|
|
|
|
// Auto-open first accordion
|
|
if (projectLogs.length > 0) {
|
|
setTimeout(() => {
|
|
const firstAccordion = document.getElementById('project-0');
|
|
if (firstAccordion) {
|
|
firstAccordion.classList.add('active');
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("Error loading VA logs:", error);
|
|
loadingState.style.display = 'none';
|
|
projectsContainer.style.display = 'none';
|
|
|
|
// Show error in empty state
|
|
emptyState.innerHTML = `
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h3>Error Loading Data</h3>
|
|
<p>${error.message}</p>
|
|
<button onclick="loadVALogs()" class="btn btn-primary" style="margin-top: 1rem;">
|
|
<i class="fas fa-redo"></i> Retry
|
|
</button>
|
|
`;
|
|
emptyState.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
// Mobile menu toggle
|
|
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
menuToggle.addEventListener('click', () => {
|
|
sidebar.classList.toggle('active');
|
|
});
|
|
|
|
// Close modal with escape key
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') {
|
|
closeLogoutModal();
|
|
}
|
|
});
|
|
|
|
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
if (e.target === this) {
|
|
closeLogoutModal();
|
|
}
|
|
});
|
|
|
|
// Logout button - opens confirmation modal instead of directly logging out
|
|
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
|
|
// Confirm logout button
|
|
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
|
|
// Load VA logs on page load
|
|
loadVALogs();
|
|
});
|
|
|
|
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |