From 5f74243c8cf3117a153968a8067d80ccff9415d5 Mon Sep 17 00:00:00 2001 From: HotSwapp <47397945+HotSwapp@users.noreply.github.com> Date: Fri, 8 Aug 2025 20:20:21 -0500 Subject: [PATCH] next front end --- app/api/admin.py | 24 +- app/main.py | 17 +- templates/admin.html | 626 +++++++++++++++++++++++++++++++++++++++++-- templates/base.html | 50 +++- 4 files changed, 665 insertions(+), 52 deletions(-) diff --git a/app/api/admin.py b/app/api/admin.py index 3546963..2003f19 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -17,6 +17,9 @@ from datetime import datetime, timedelta, date from pathlib import Path from app.database.base import get_db + +# Track application start time +APPLICATION_START_TIME = time.time() from app.models import User, Rolodex, File as FileModel, Ledger, QDRO, AuditLog, LoginAttempt from app.models.lookups import SystemSetup, Employee, FileType, FileStatus, TransactionType, TransactionCode, State, FormIndex from app.auth.security import get_admin_user, get_password_hash, create_access_token @@ -219,13 +222,9 @@ async def system_health( except: alerts.append("Unable to check backup status") - # System uptime (simplified) - try: - import psutil - uptime_seconds = time.time() - psutil.boot_time() - uptime = str(timedelta(seconds=int(uptime_seconds))) - except ImportError: - uptime = "Unknown" + # Application uptime + uptime_seconds = int(time.time() - APPLICATION_START_TIME) + uptime = str(timedelta(seconds=uptime_seconds)) status = "healthy" if db_connected and disk_available and memory_available else "unhealthy" @@ -287,14 +286,9 @@ async def system_statistics( except: pass - # System uptime (simplified) - system_uptime = "Unknown" - try: - import psutil - uptime_seconds = time.time() - psutil.boot_time() - system_uptime = str(timedelta(seconds=int(uptime_seconds))) - except ImportError: - pass + # Application uptime + uptime_seconds = int(time.time() - APPLICATION_START_TIME) + system_uptime = str(timedelta(seconds=uptime_seconds)) # Recent activity (last 10 actions) recent_activity = [] diff --git a/app/main.py b/app/main.py index 110fdda..0b168b7 100644 --- a/app/main.py +++ b/app/main.py @@ -1,15 +1,17 @@ """ Delphi Consulting Group Database System - Main FastAPI Application """ -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, Depends from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.middleware.cors import CORSMiddleware from app.config import settings from app.database.base import engine from app.models import BaseModel +from app.models.user import User +from app.auth.security import get_admin_user # Create database tables BaseModel.metadata.create_all(bind=engine) @@ -54,7 +56,7 @@ app.include_router(financial_router, prefix="/api/financial", tags=["financial"] app.include_router(documents_router, prefix="/api/documents", tags=["documents"]) app.include_router(search_router, prefix="/api/search", tags=["search"]) app.include_router(admin_router, prefix="/api/admin", tags=["admin"]) -app.include_router(import_router, tags=["import"]) +app.include_router(import_router, prefix="/api/import", tags=["import"]) app.include_router(support_router, prefix="/api/support", tags=["support"]) @@ -85,13 +87,6 @@ async def customers_page(request: Request): ) -@app.get("/import", response_class=HTMLResponse) -async def import_page(request: Request): - """Data import management page""" - return templates.TemplateResponse( - "import.html", - {"request": request, "title": "Data Import - " + settings.app_name} - ) @app.get("/files", response_class=HTMLResponse) @@ -132,7 +127,7 @@ async def search_page(request: Request): @app.get("/admin", response_class=HTMLResponse) async def admin_page(request: Request): - """System administration page""" + """System administration page (admin only)""" return templates.TemplateResponse( "admin.html", {"request": request, "title": "System Administration - " + settings.app_name} diff --git a/templates/admin.html b/templates/admin.html index 4f30d88..67b250f 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -325,6 +325,147 @@ + +
+
+
+

Data Import Management

+ + +
+
+
Current Database Status
+ +
+
+
+
+
+ Loading... +
+

Loading import status...

+
+
+
+
+ + +
+
+
Upload CSV Files
+
+
+
+
+
+ + +
+
+
+ + +
Select the CSV file to import
+
+
+ +
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ + + + + + + + + + + +
+
+
Data Management
+
+
+
+
+
Clear Table Data
+

Remove all records from a specific table (cannot be undone)

+
+ + +
+
+
+
Quick Actions
+
+ +
+
+
+
+
+
+
+
+
@@ -813,24 +954,66 @@ function getAuthHeaders() { }; } +// Check if current user has admin access +async function checkAdminAccess() { + const token = localStorage.getItem('auth_token'); + if (!token) { + window.location.href = '/login'; + return false; + } + + try { + const response = await fetch('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) { + window.location.href = '/login'; + return false; + } + + const user = await response.json(); + return user.is_admin === true; + } catch (error) { + console.error('Error checking admin access:', error); + window.location.href = '/login'; + return false; + } +} + // Initialize admin dashboard document.addEventListener('DOMContentLoaded', function() { - loadSystemHealth(); - loadSystemStats(); - loadUsers(); - loadSettings(); - loadLookupTables(); - loadBackups(); - loadIssues(); - loadIssueStats(); - - // Auto-refresh every 5 minutes - setInterval(loadSystemHealth, 300000); - - // Load issue stats when Issue Tracking tab is clicked - document.getElementById('issues-tab').addEventListener('shown.bs.tab', function() { - loadIssues(); - loadIssueStats(); + // Check admin permissions first + checkAdminAccess().then(hasAccess => { + if (!hasAccess) { + window.location.href = '/customers'; // Redirect to a safe page + return; + } + + loadSystemHealth(); + loadSystemStats(); + loadUsers(); + loadSettings(); + loadLookupTables(); + loadBackups(); + // Don't load issues and stats on initial load - only when tab is clicked + + // Auto-refresh every 5 minutes + setInterval(loadSystemHealth, 300000); + + // Load issue stats when Issue Tracking tab is clicked + document.getElementById('issues-tab').addEventListener('shown.bs.tab', function() { + loadIssues(); + loadIssueStats(); + }); + + // Load import data when Import tab is clicked + document.getElementById('import-tab').addEventListener('shown.bs.tab', function() { + loadAvailableImportFiles(); + loadImportStatus(); + }); }); }); @@ -854,8 +1037,25 @@ async function loadSystemHealth() { statusTextElement.textContent = 'Unhealthy'; } - // Update system info - document.getElementById('system-uptime').textContent = data.uptime; + // Update system info with better formatting + const formatUptime = (uptime) => { + if (!uptime || uptime === 'Unknown') return 'Unknown'; + + // Parse the uptime string (format: "0:00:05" or "1 day, 0:00:05") + const parts = uptime.split(', '); + if (parts.length === 1) { + // Less than a day: "0:00:05" + const [hours, minutes, seconds] = parts[0].split(':'); + return `${parseInt(hours)}h ${parseInt(minutes)}m ${parseInt(seconds)}s`; + } else { + // More than a day: "1 day, 0:00:05" + const days = parts[0]; + const [hours, minutes, seconds] = parts[1].split(':'); + return `${days}, ${parseInt(hours)}h ${parseInt(minutes)}m`; + } + }; + + document.getElementById('system-uptime').textContent = formatUptime(data.uptime); // Update alerts const alertsElement = document.getElementById('system-alerts'); @@ -1414,7 +1614,13 @@ function refreshDashboard() { loadSettings(); loadLookupTables(); loadBackups(); - loadIssues(); + + // Only refresh issues if on issues tab + if (document.getElementById('issues-tab').classList.contains('active')) { + loadIssues(); + loadIssueStats(); + } + showAlert('Dashboard refreshed', 'info'); } @@ -1468,10 +1674,17 @@ async function loadIssueStats() { const stats = await response.json(); // Update dashboard cards - document.getElementById('high-priority-count').textContent = stats.high_priority_tickets + stats.urgent_tickets; - document.getElementById('open-count').textContent = stats.open_tickets; - document.getElementById('in-progress-count').textContent = stats.in_progress_tickets; - document.getElementById('resolved-count').textContent = stats.resolved_tickets; + const updateElement = (id, value) => { + const element = document.getElementById(id); + if (element) { + element.textContent = value || 0; + } + }; + + updateElement('high-priority-count', (stats.high_priority_tickets || 0) + (stats.urgent_tickets || 0)); + updateElement('open-issues-count', stats.open_tickets || 0); + updateElement('in-progress-count', stats.in_progress_tickets || 0); + updateElement('resolved-count', stats.resolved_tickets || 0); } catch (error) { console.error('Failed to load issue stats:', error); @@ -1768,6 +1981,373 @@ async function addResponse() { } } +// Import Management Functions +let availableImportFiles = {}; +let importInProgress = false; + +async function loadAvailableImportFiles() { + try { + const response = await fetch('/api/import/available-files', { + headers: getAuthHeaders() + }); + + if (!response.ok) throw new Error('Failed to load available files'); + + const data = await response.json(); + availableImportFiles = data; + + // Populate file type dropdowns + const fileTypeSelect = document.getElementById('adminFileType'); + const clearTableSelect = document.getElementById('adminClearTableType'); + + fileTypeSelect.innerHTML = ''; + clearTableSelect.innerHTML = ''; + + data.available_files.forEach(fileType => { + const description = data.descriptions[fileType] || fileType; + + const option1 = document.createElement('option'); + option1.value = fileType; + option1.textContent = `${fileType} - ${description}`; + fileTypeSelect.appendChild(option1); + + const option2 = document.createElement('option'); + option2.value = fileType; + option2.textContent = `${fileType} - ${description}`; + clearTableSelect.appendChild(option2); + }); + + // Setup form listener + document.getElementById('adminImportForm').addEventListener('submit', handleAdminImport); + + // File type change listener + document.getElementById('adminFileType').addEventListener('change', updateAdminFileTypeDescription); + + } catch (error) { + console.error('Error loading available files:', error); + showAlert('Error loading available file types: ' + error.message, 'error'); + } +} + +async function loadImportStatus() { + try { + const response = await fetch('/api/import/status', { + headers: getAuthHeaders() + }); + + if (!response.ok) throw new Error('Failed to load import status'); + + const status = await response.json(); + displayImportStatus(status); + + } catch (error) { + console.error('Error loading import status:', error); + document.getElementById('importStatus').innerHTML = + '
Error loading import status: ' + error.message + '
'; + } +} + +function displayImportStatus(status) { + const container = document.getElementById('importStatus'); + + let html = '
'; + let totalRecords = 0; + + Object.entries(status).forEach(([fileType, info], index) => { + totalRecords += info.record_count || 0; + + const statusClass = info.error ? 'danger' : (info.record_count > 0 ? 'success' : 'secondary'); + const statusIcon = info.error ? 'exclamation-triangle' : (info.record_count > 0 ? 'check-circle' : 'circle'); + + if (index % 3 === 0 && index > 0) { + html += '
'; + } + + html += ` +
+
+
+
+
+ ${fileType}
+ ${info.table_name} +
+
+
+ ${info.record_count || 0} +
+
+ ${info.error ? `
${info.error}
` : ''} +
+
+
+ `; + }); + + html += '
'; + html += `
+ Total Records: ${totalRecords.toLocaleString()} +
`; + + container.innerHTML = html; +} + +function updateAdminFileTypeDescription() { + const fileType = document.getElementById('adminFileType').value; + const description = availableImportFiles.descriptions && availableImportFiles.descriptions[fileType]; + document.getElementById('adminFileTypeDescription').textContent = description || ''; +} + +async function validateAdminFile() { + const fileType = document.getElementById('adminFileType').value; + const fileInput = document.getElementById('adminCsvFile'); + + if (!fileType || !fileInput.files[0]) { + showAlert('Please select both file type and CSV file', 'error'); + return; + } + + const formData = new FormData(); + formData.append('file', fileInput.files[0]); + + try { + showAdminProgress(true, 'Validating file...'); + + const response = await fetch(`/api/import/validate/${fileType}`, { + method: 'POST', + headers: getAuthHeaders(), + body: formData + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Validation failed'); + } + + const result = await response.json(); + displayAdminValidationResults(result); + + } catch (error) { + console.error('Validation error:', error); + showAlert('Validation failed: ' + error.message, 'error'); + } finally { + showAdminProgress(false); + } +} + +function displayAdminValidationResults(result) { + const panel = document.getElementById('adminValidationPanel'); + const container = document.getElementById('adminValidationResults'); + + let html = ''; + + // Overall status + const statusClass = result.valid ? 'success' : 'danger'; + const statusIcon = result.valid ? 'check-circle-fill' : 'exclamation-triangle-fill'; + + html += ` +
+ + File validation ${result.valid ? 'passed' : 'failed'} +
+ `; + + // Headers validation + html += '
Column Headers
'; + if (result.headers.missing.length > 0) { + html += `
+ Missing columns: ${result.headers.missing.join(', ')} +
`; + } + if (result.headers.extra.length > 0) { + html += `
+ Extra columns: ${result.headers.extra.join(', ')} +
`; + } + if (result.headers.missing.length === 0 && result.headers.extra.length === 0) { + html += '
All expected columns found
'; + } + + // Sample data + if (result.sample_data && result.sample_data.length > 0) { + html += '
Sample Data (First 10 rows)
'; + html += '
'; + html += ''; + html += ''; + Object.keys(result.sample_data[0]).forEach(header => { + html += ``; + }); + html += ''; + + result.sample_data.forEach(row => { + html += ''; + Object.values(row).forEach(value => { + html += ``; + }); + html += ''; + }); + html += '
${header}
${value || ''}
'; + } + + // Validation errors + if (result.validation_errors && result.validation_errors.length > 0) { + html += '
Data Issues Found
'; + html += '
'; + result.validation_errors.forEach(error => { + html += `
Row ${error.row}, Field "${error.field}": ${error.error}
`; + }); + if (result.total_errors > result.validation_errors.length) { + html += `
... and ${result.total_errors - result.validation_errors.length} more errors
`; + } + html += '
'; + } + + container.innerHTML = html; + panel.style.display = 'block'; +} + +async function handleAdminImport(event) { + event.preventDefault(); + + if (importInProgress) { + showAlert('Import already in progress', 'error'); + return; + } + + const fileType = document.getElementById('adminFileType').value; + const fileInput = document.getElementById('adminCsvFile'); + const replaceExisting = document.getElementById('adminReplaceExisting').checked; + + if (!fileType || !fileInput.files[0]) { + showAlert('Please select both file type and CSV file', 'error'); + return; + } + + importInProgress = true; + + const formData = new FormData(); + formData.append('file', fileInput.files[0]); + formData.append('replace_existing', replaceExisting); + + try { + showAdminProgress(true, 'Importing data...'); + + const response = await fetch(`/api/import/upload/${fileType}`, { + method: 'POST', + headers: getAuthHeaders(), + body: formData + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Import failed'); + } + + const result = await response.json(); + displayAdminImportResults(result); + + // Refresh status after successful import + await loadImportStatus(); + + // Reset form + document.getElementById('adminImportForm').reset(); + + } catch (error) { + console.error('Import error:', error); + showAlert('Import failed: ' + error.message, 'error'); + } finally { + importInProgress = false; + showAdminProgress(false); + } +} + +function displayAdminImportResults(result) { + const panel = document.getElementById('adminResultsPanel'); + const container = document.getElementById('adminImportResults'); + + const successClass = result.errors && result.errors.length > 0 ? 'warning' : 'success'; + + let html = ` +
+
Import Completed
+

+ File Type: ${result.file_type}
+ Records Imported: ${result.imported_count}
+ Errors: ${result.total_errors || 0} +

+
+ `; + + if (result.errors && result.errors.length > 0) { + html += '
Import Errors
'; + html += '
'; + result.errors.forEach(error => { + html += `
Row ${error.row}: ${error.error}
`; + }); + if (result.total_errors > result.errors.length) { + html += `
... and ${result.total_errors - result.errors.length} more errors
`; + } + html += '
'; + } + + container.innerHTML = html; + panel.style.display = 'block'; +} + +function showAdminProgress(show, message = '') { + const panel = document.getElementById('adminProgressPanel'); + const status = document.getElementById('adminProgressStatus'); + const bar = document.getElementById('adminProgressBar'); + + if (show) { + status.textContent = message; + bar.style.width = '100%'; + bar.textContent = 'Processing...'; + panel.style.display = 'block'; + } else { + panel.style.display = 'none'; + } +} + +async function clearAdminTable() { + const fileType = document.getElementById('adminClearTableType').value; + + if (!fileType) { + showAlert('Please select a table to clear', 'error'); + return; + } + + if (!confirm(`Are you sure you want to clear all data from ${fileType}? This action cannot be undone.`)) { + return; + } + + try { + const response = await fetch(`/api/import/clear/${fileType}`, { + method: 'DELETE', + headers: getAuthHeaders() + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Clear operation failed'); + } + + const result = await response.json(); + showAlert(`Successfully cleared ${result.deleted_count} records from ${result.table_name}`, 'success'); + + // Refresh status + await loadImportStatus(); + + } catch (error) { + console.error('Clear table error:', error); + showAlert('Clear operation failed: ' + error.message, 'error'); + } +} + +function viewImportLogs() { + showAlert('Import logs functionality coming soon', 'info'); +} + function searchUsers() { const searchTerm = document.getElementById('user-search').value.toLowerCase(); const filteredUsers = currentUsers.filter(user => diff --git a/templates/base.html b/templates/base.html index 5591622..99117dc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -99,8 +99,8 @@ User @@ -122,7 +122,7 @@
- © 2024 Delphi Consulting Group Database System + © Delphi Consulting Group Database System | Loading... @@ -218,7 +218,9 @@ document.addEventListener('DOMContentLoaded', function() { initializeKeyboardShortcuts(); updateCurrentPageDisplay(); + updateCurrentYear(); initializeAuthManager(); + checkUserPermissions(); }); // Update current page display in footer @@ -243,6 +245,48 @@ } } + // Update current year in footer + function updateCurrentYear() { + const currentYear = new Date().getFullYear(); + const yearElement = document.getElementById('currentYear'); + if (yearElement) { + yearElement.textContent = currentYear; + } + } + + // Check user permissions and show/hide admin menu + async function checkUserPermissions() { + const token = localStorage.getItem('auth_token'); + if (!token || isLoginPage()) { + return; + } + + try { + const response = await fetch('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const user = await response.json(); + if (user.is_admin) { + // Show admin menu items + document.getElementById('admin-menu-item').style.display = 'block'; + document.getElementById('admin-menu-divider').style.display = 'block'; + } + + // Update user display name if available + const userDropdown = document.querySelector('.nav-link.dropdown-toggle'); + if (user.full_name && userDropdown) { + userDropdown.innerHTML = ` ${user.full_name}`; + } + } + } catch (error) { + console.error('Error checking user permissions:', error); + } + } + // Authentication Manager function initializeAuthManager() { // Check if we have a valid token on page load