Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Legal Dashboard - Reports & Analytics</title> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <!-- Load API Client and Core System --> | |
| <script src="js/api-client.js"></script> | |
| <script src="js/core.js"></script> | |
| <script src="js/notifications.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #007bff; | |
| --success-color: #28a745; | |
| --warning-color: #ffc107; | |
| --danger-color: #dc3545; | |
| --info-color: #17a2b8; | |
| } | |
| body { | |
| background-color: #f8f9fa; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| .navbar { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| .navbar-brand { | |
| font-weight: bold; | |
| color: white ; | |
| } | |
| .nav-link { | |
| color: rgba(255,255,255,0.9) ; | |
| transition: color 0.3s ease; | |
| } | |
| .nav-link:hover { | |
| color: white ; | |
| } | |
| .main-content { | |
| padding: 2rem 0; | |
| } | |
| .card { | |
| border: none; | |
| border-radius: 15px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| margin-bottom: 1.5rem; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 8px 30px rgba(0,0,0,0.15); | |
| } | |
| .card-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border-radius: 15px 15px 0 0 ; | |
| padding: 1.5rem; | |
| } | |
| .metric-card { | |
| text-align: center; | |
| padding: 2rem; | |
| } | |
| .metric-value { | |
| font-size: 2.5rem; | |
| font-weight: bold; | |
| margin-bottom: 0.5rem; | |
| } | |
| .metric-label { | |
| color: #666; | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .metric-change { | |
| font-size: 0.8rem; | |
| margin-top: 0.5rem; | |
| } | |
| .metric-change.positive { | |
| color: var(--success-color); | |
| } | |
| .metric-change.negative { | |
| color: var(--danger-color); | |
| } | |
| .chart-container { | |
| position: relative; | |
| height: 400px; | |
| margin: 1rem 0; | |
| } | |
| .table-responsive { | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| .table { | |
| margin-bottom: 0; | |
| } | |
| .table thead th { | |
| background-color: #f8f9fa; | |
| border: none; | |
| font-weight: 600; | |
| color: #495057; | |
| } | |
| .table tbody tr { | |
| transition: background-color 0.2s ease; | |
| } | |
| .table tbody tr:hover { | |
| background-color: #f8f9fa; | |
| } | |
| .btn-export { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border: none; | |
| color: white; | |
| padding: 0.5rem 1.5rem; | |
| border-radius: 25px; | |
| transition: all 0.3s ease; | |
| } | |
| .btn-export:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| color: white; | |
| } | |
| .status-badge { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| font-weight: 500; | |
| } | |
| .status-badge.success { | |
| background-color: #d4edda; | |
| color: #155724; | |
| } | |
| .status-badge.warning { | |
| background-color: #fff3cd; | |
| color: #856404; | |
| } | |
| .status-badge.error { | |
| background-color: #f8d7da; | |
| color: #721c24; | |
| } | |
| .loading { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 200px; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid var(--primary-color); | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .filter-section { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
| } | |
| .date-range { | |
| display: flex; | |
| gap: 1rem; | |
| align-items: center; | |
| } | |
| .date-input { | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| padding: 0.5rem; | |
| font-size: 0.9rem; | |
| } | |
| .refresh-btn { | |
| background: var(--primary-color); | |
| border: none; | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .refresh-btn:hover { | |
| background: #0056b3; | |
| transform: translateY(-1px); | |
| } | |
| .export-section { | |
| display: flex; | |
| gap: 1rem; | |
| flex-wrap: wrap; | |
| margin-top: 1rem; | |
| } | |
| .progress-ring { | |
| width: 120px; | |
| height: 120px; | |
| margin: 0 auto; | |
| } | |
| .progress-ring circle { | |
| fill: none; | |
| stroke-width: 8; | |
| } | |
| .progress-ring .bg { | |
| stroke: #e9ecef; | |
| } | |
| .progress-ring .progress { | |
| stroke: var(--primary-color); | |
| stroke-linecap: round; | |
| transition: stroke-dasharray 0.3s ease; | |
| } | |
| .progress-text { | |
| text-align: center; | |
| margin-top: 1rem; | |
| } | |
| .progress-value { | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| color: var(--primary-color); | |
| } | |
| .progress-label { | |
| font-size: 0.9rem; | |
| color: #666; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Navigation --> | |
| <nav class="navbar navbar-expand-lg navbar-dark"> | |
| <div class="container"> | |
| <a class="navbar-brand" href="#"> | |
| <i class="fas fa-chart-line me-2"></i> | |
| Legal Dashboard - Reports | |
| </a> | |
| <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-label="Toggle navigation"> | |
| <span class="navbar-toggler-icon"></span> | |
| </button> | |
| <div class="collapse navbar-collapse" id="navbarNav"> | |
| <ul class="navbar-nav ms-auto"> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="index.html"> | |
| <i class="fas fa-home me-1"></i>Dashboard | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="documents.html"> | |
| <i class="fas fa-file-alt me-1"></i>Documents | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link active" href="reports.html"> | |
| <i class="fas fa-chart-bar me-1"></i>Reports | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="#" onclick="logout()"> | |
| <i class="fas fa-sign-out-alt me-1"></i>Logout | |
| </a> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content --> | |
| <div class="container main-content"> | |
| <!-- Filter Section --> | |
| <div class="filter-section"> | |
| <div class="row align-items-center"> | |
| <div class="col-md-6"> | |
| <h5 class="mb-3"> | |
| <i class="fas fa-filter me-2"></i>Filter Options | |
| </h5> | |
| <div class="date-range"> | |
| <label class="me-2">Date Range:</label> | |
| <input type="date" id="startDate" class="date-input"> | |
| <span>to</span> | |
| <input type="date" id="endDate" class="date-input"> | |
| <button type="button" class="refresh-btn" onclick="loadReports()"> | |
| <i class="fas fa-sync-alt me-1"></i>Refresh | |
| </button> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="export-section"> | |
| <button type="button" class="btn btn-export" onclick="exportReport('summary')"> | |
| <i class="fas fa-download me-1"></i>Export Summary | |
| </button> | |
| <button type="button" class="btn btn-export" onclick="exportReport('user_activity')"> | |
| <i class="fas fa-users me-1"></i>Export User Activity | |
| </button> | |
| <button type="button" class="btn btn-export" onclick="exportReport('document_analytics')"> | |
| <i class="fas fa-file-alt me-1"></i>Export Document Analytics | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Summary Metrics --> | |
| <div class="row" id="summaryMetrics"> | |
| <div class="col-md-3"> | |
| <div class="card metric-card"> | |
| <div class="metric-value" id="totalDocuments">-</div> | |
| <div class="metric-label">Total Documents</div> | |
| <div class="metric-change positive" id="documentsChange">+12% from last month</div> | |
| </div> | |
| </div> | |
| <div class="col-md-3"> | |
| <div class="card metric-card"> | |
| <div class="metric-value" id="totalUsers">-</div> | |
| <div class="metric-label">Active Users</div> | |
| <div class="metric-change positive" id="usersChange">+5% from last month</div> | |
| </div> | |
| </div> | |
| <div class="col-md-3"> | |
| <div class="card metric-card"> | |
| <div class="metric-value" id="successRate">-</div> | |
| <div class="metric-label">Success Rate</div> | |
| <div class="metric-change positive" id="successChange">+3% from last month</div> | |
| </div> | |
| </div> | |
| <div class="col-md-3"> | |
| <div class="card metric-card"> | |
| <div class="metric-value" id="avgProcessingTime">-</div> | |
| <div class="metric-label">Avg Processing Time</div> | |
| <div class="metric-change negative" id="timeChange">+0.5s from last month</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Charts Row --> | |
| <div class="row"> | |
| <div class="col-md-8"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h5 class="mb-0"> | |
| <i class="fas fa-chart-line me-2"></i>Document Processing Trends | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="chart-container"> | |
| <canvas id="trendsChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-4"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h5 class="mb-0"> | |
| <i class="fas fa-pie-chart me-2"></i>Processing Status | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="chart-container"> | |
| <canvas id="statusChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Performance Metrics --> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h5 class="mb-0"> | |
| <i class="fas fa-tachometer-alt me-2"></i>System Performance | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="row"> | |
| <div class="col-6"> | |
| <div class="progress-ring"> | |
| <svg width="120" height="120"> | |
| <circle class="bg" cx="60" cy="60" r="50"></circle> | |
| <circle class="progress" cx="60" cy="60" r="50" | |
| stroke-dasharray="314" stroke-dashoffset="78.5"></circle> | |
| </svg> | |
| </div> | |
| <div class="progress-text"> | |
| <div class="progress-value">75%</div> | |
| <div class="progress-label">CPU Usage</div> | |
| </div> | |
| </div> | |
| <div class="col-6"> | |
| <div class="progress-ring"> | |
| <svg width="120" height="120"> | |
| <circle class="bg" cx="60" cy="60" r="50"></circle> | |
| <circle class="progress" cx="60" cy="60" r="50" | |
| stroke-dasharray="314" stroke-dashoffset="157"></circle> | |
| </svg> | |
| </div> | |
| <div class="progress-text"> | |
| <div class="progress-value">50%</div> | |
| <div class="progress-label">Memory Usage</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h5 class="mb-0"> | |
| <i class="fas fa-clock me-2"></i>Response Times | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="chart-container"> | |
| <canvas id="responseChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- User Activity Table --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h5 class="mb-0"> | |
| <i class="fas fa-users me-2"></i>User Activity | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="table-responsive"> | |
| <table class="table table-hover" id="userActivityTable"> | |
| <thead> | |
| <tr> | |
| <th scope="col">User</th> | |
| <th scope="col">Documents Processed</th> | |
| <th scope="col">Last Activity</th> | |
| <th scope="col">Success Rate</th> | |
| <th scope="col">Avg Processing Time</th> | |
| </tr> | |
| </thead> | |
| <tbody id="userActivityBody"> | |
| <tr> | |
| <td colspan="5" class="text-center"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Document Analytics Table --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h5 class="mb-0"> | |
| <i class="fas fa-file-alt me-2"></i>Recent Document Analytics | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="table-responsive"> | |
| <table class="table table-hover" id="documentAnalyticsTable"> | |
| <thead> | |
| <tr> | |
| <th scope="col">Document</th> | |
| <th scope="col">Processing Time</th> | |
| <th scope="col">OCR Accuracy</th> | |
| <th scope="col">File Size</th> | |
| <th scope="col">Status</th> | |
| <th scope="col">Created</th> | |
| </tr> | |
| </thead> | |
| <tbody id="documentAnalyticsBody"> | |
| <tr> | |
| <td colspan="6" class="text-center"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Scripts --> | |
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> | |
| <script src="js/notifications.js"></script> | |
| <script> | |
| // Global variables | |
| let trendsChart, statusChart, responseChart; | |
| let currentData = {}; | |
| // Initialize page | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Set default date range (last 30 days) | |
| const endDate = new Date(); | |
| const startDate = new Date(); | |
| startDate.setDate(startDate.getDate() - 30); | |
| document.getElementById('endDate').value = endDate.toISOString().split('T')[0]; | |
| document.getElementById('startDate').value = startDate.toISOString().split('T')[0]; | |
| // Load initial data | |
| loadReports(); | |
| }); | |
| // Load reports data | |
| async function loadReports() { | |
| try { | |
| showLoading(); | |
| // Load summary metrics | |
| await loadSummaryMetrics(); | |
| // Load charts | |
| await loadCharts(); | |
| // Load tables | |
| await loadUserActivity(); | |
| await loadDocumentAnalytics(); | |
| hideLoading(); | |
| } catch (error) { | |
| console.error('Error loading reports:', error); | |
| showNotification('error', 'Error', 'Failed to load reports data'); | |
| hideLoading(); | |
| } | |
| } | |
| // Load summary metrics | |
| async function loadSummaryMetrics() { | |
| try { | |
| const response = await fetch('/api/reports/summary', { | |
| headers: { | |
| 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
| } | |
| }); | |
| if (!response.ok) throw new Error('Failed to load summary'); | |
| const data = await response.json(); | |
| currentData.summary = data; | |
| // Update metrics | |
| document.getElementById('totalDocuments').textContent = data.total_documents; | |
| document.getElementById('totalUsers').textContent = data.total_users; | |
| document.getElementById('successRate').textContent = `${data.success_rate.toFixed(1)}%`; | |
| document.getElementById('avgProcessingTime').textContent = `${data.avg_processing_time.toFixed(1)}s`; | |
| } catch (error) { | |
| console.error('Error loading summary metrics:', error); | |
| // Set default values | |
| document.getElementById('totalDocuments').textContent = '0'; | |
| document.getElementById('totalUsers').textContent = '0'; | |
| document.getElementById('successRate').textContent = '0%'; | |
| document.getElementById('avgProcessingTime').textContent = '0s'; | |
| } | |
| } | |
| // Load charts | |
| async function loadCharts() { | |
| try { | |
| // Load trends data | |
| const trendsResponse = await fetch('/api/reports/trends?days=30', { | |
| headers: { | |
| 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
| } | |
| }); | |
| if (trendsResponse.ok) { | |
| const trendsData = await trendsResponse.json(); | |
| createTrendsChart(trendsData.daily_trends); | |
| } | |
| // Create status chart (mock data for now) | |
| createStatusChart(); | |
| // Create response time chart (mock data for now) | |
| createResponseChart(); | |
| } catch (error) { | |
| console.error('Error loading charts:', error); | |
| } | |
| } | |
| // Create trends chart | |
| function createTrendsChart(data) { | |
| const ctx = document.getElementById('trendsChart').getContext('2d'); | |
| if (trendsChart) { | |
| trendsChart.destroy(); | |
| } | |
| trendsChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: data.map(d => d.date), | |
| datasets: [{ | |
| label: 'Documents Processed', | |
| data: data.map(d => d.documents_processed), | |
| borderColor: '#667eea', | |
| backgroundColor: 'rgba(102, 126, 234, 0.1)', | |
| tension: 0.4 | |
| }, { | |
| label: 'Success Rate (%)', | |
| data: data.map(d => d.success_rate), | |
| borderColor: '#28a745', | |
| backgroundColor: 'rgba(40, 167, 69, 0.1)', | |
| tension: 0.4, | |
| yAxisID: 'y1' | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| title: { | |
| display: true, | |
| text: 'Documents' | |
| } | |
| }, | |
| y1: { | |
| position: 'right', | |
| beginAtZero: true, | |
| max: 100, | |
| title: { | |
| display: true, | |
| text: 'Success Rate (%)' | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top' | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Create status chart | |
| function createStatusChart() { | |
| const ctx = document.getElementById('statusChart').getContext('2d'); | |
| if (statusChart) { | |
| statusChart.destroy(); | |
| } | |
| statusChart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Completed', 'Processing', 'Failed', 'Pending'], | |
| datasets: [{ | |
| data: [65, 15, 10, 10], | |
| backgroundColor: [ | |
| '#28a745', | |
| '#ffc107', | |
| '#dc3545', | |
| '#6c757d' | |
| ] | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'bottom' | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Create response time chart | |
| function createResponseChart() { | |
| const ctx = document.getElementById('responseChart').getContext('2d'); | |
| if (responseChart) { | |
| responseChart.destroy(); | |
| } | |
| responseChart = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: ['Documents', 'OCR', 'Search', 'Analytics'], | |
| datasets: [{ | |
| label: 'Response Time (ms)', | |
| data: [150, 2500, 200, 300], | |
| backgroundColor: [ | |
| '#667eea', | |
| '#764ba2', | |
| '#f093fb', | |
| '#f5576c' | |
| ] | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| title: { | |
| display: true, | |
| text: 'Time (ms)' | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Load user activity | |
| async function loadUserActivity() { | |
| try { | |
| const response = await fetch('/api/reports/user-activity?days=30', { | |
| headers: { | |
| 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
| } | |
| }); | |
| if (!response.ok) throw new Error('Failed to load user activity'); | |
| const data = await response.json(); | |
| currentData.userActivity = data; | |
| const tbody = document.getElementById('userActivityBody'); | |
| tbody.innerHTML = ''; | |
| data.forEach(user => { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td><strong>${user.username}</strong></td> | |
| <td>${user.documents_processed}</td> | |
| <td>${formatDate(user.last_activity)}</td> | |
| <td><span class="status-badge success">${user.success_rate.toFixed(1)}%</span></td> | |
| <td>${user.total_processing_time.toFixed(1)}s</td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } catch (error) { | |
| console.error('Error loading user activity:', error); | |
| document.getElementById('userActivityBody').innerHTML = | |
| '<tr><td colspan="5" class="text-center text-muted">No data available</td></tr>'; | |
| } | |
| } | |
| // Load document analytics | |
| async function loadDocumentAnalytics() { | |
| try { | |
| const response = await fetch('/api/reports/document-analytics?limit=20', { | |
| headers: { | |
| 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
| } | |
| }); | |
| if (!response.ok) throw new Error('Failed to load document analytics'); | |
| const data = await response.json(); | |
| currentData.documentAnalytics = data; | |
| const tbody = document.getElementById('documentAnalyticsBody'); | |
| tbody.innerHTML = ''; | |
| data.forEach(doc => { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td><strong>${doc.filename}</strong></td> | |
| <td>${doc.processing_time.toFixed(1)}s</td> | |
| <td>${doc.ocr_accuracy ? doc.ocr_accuracy.toFixed(1) + '%' : 'N/A'}</td> | |
| <td>${formatFileSize(doc.file_size)}</td> | |
| <td><span class="status-badge ${getStatusClass(doc.status)}">${doc.status}</span></td> | |
| <td>${formatDate(doc.created_at)}</td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } catch (error) { | |
| console.error('Error loading document analytics:', error); | |
| document.getElementById('documentAnalyticsBody').innerHTML = | |
| '<tr><td colspan="6" class="text-center text-muted">No data available</td></tr>'; | |
| } | |
| } | |
| // Export report | |
| async function exportReport(type) { | |
| try { | |
| const response = await fetch(`/api/reports/export/csv?report_type=${type}`, { | |
| headers: { | |
| 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
| } | |
| }); | |
| if (!response.ok) throw new Error('Failed to export report'); | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${type}_report_${new Date().toISOString().split('T')[0]}.csv`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| window.URL.revokeObjectURL(url); | |
| showNotification('success', 'Export Successful', `${type} report has been downloaded`); | |
| } catch (error) { | |
| console.error('Error exporting report:', error); | |
| showNotification('error', 'Export Failed', 'Failed to export report'); | |
| } | |
| } | |
| // Utility functions | |
| function formatDate(dateString) { | |
| if (!dateString || dateString === 'Never') return 'Never'; | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); | |
| } | |
| function formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| function getStatusClass(status) { | |
| switch (status.toLowerCase()) { | |
| case 'completed': return 'success'; | |
| case 'processing': return 'warning'; | |
| case 'failed': return 'error'; | |
| default: return 'warning'; | |
| } | |
| } | |
| function showLoading() { | |
| // Show loading indicators | |
| const loadingElements = document.querySelectorAll('.loading'); | |
| loadingElements.forEach(el => el.style.display = 'flex'); | |
| } | |
| function hideLoading() { | |
| // Hide loading indicators | |
| const loadingElements = document.querySelectorAll('.loading'); | |
| loadingElements.forEach(el => el.style.display = 'none'); | |
| } | |
| function logout() { | |
| localStorage.removeItem('auth_token'); | |
| window.location.href = 'index.html'; | |
| } | |
| // Auto-refresh every 5 minutes | |
| setInterval(loadReports, 300000); | |
| </script> | |
| </body> | |
| </html> |