next front end

This commit is contained in:
HotSwapp
2025-08-08 20:20:21 -05:00
parent 04edc636f8
commit 5f74243c8c
4 changed files with 665 additions and 52 deletions

View File

@@ -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 = []

View File

@@ -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}

View File

@@ -325,6 +325,147 @@
</div>
</div>
<!-- Data Import Tab -->
<div class="tab-pane fade" id="import" role="tabpanel">
<div class="row">
<div class="col-12">
<h4 class="mb-4"><i class="bi bi-upload"></i> Data Import Management</h4>
<!-- Import Status Panel -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Current Database Status</h5>
<button class="btn btn-outline-info btn-sm" onclick="loadImportStatus()">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
<div class="card-body">
<div id="importStatus">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading import status...</p>
</div>
</div>
</div>
</div>
<!-- CSV File Upload Panel -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-file-earmark-arrow-up"></i> Upload CSV Files</h5>
</div>
<div class="card-body">
<form id="adminImportForm" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-md-4">
<label for="adminFileType" class="form-label">Data Type *</label>
<select class="form-select" id="adminFileType" name="fileType" required>
<option value="">Select data type...</option>
</select>
<div class="form-text" id="adminFileTypeDescription"></div>
</div>
<div class="col-md-6">
<label for="adminCsvFile" class="form-label">CSV File *</label>
<input type="file" class="form-control" id="adminCsvFile" name="csvFile" accept=".csv" required>
<div class="form-text">Select the CSV file to import</div>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="adminReplaceExisting" name="replaceExisting">
<label class="form-check-label" for="adminReplaceExisting">
Replace existing data
</label>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="btn-group" role="group">
<button type="button" class="btn btn-secondary" onclick="validateAdminFile()">
<i class="bi bi-check-circle"></i> Validate File
</button>
<button type="submit" class="btn btn-primary" id="adminImportBtn">
<i class="bi bi-upload"></i> Import Data
</button>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Validation Results Panel -->
<div class="card mb-4" id="adminValidationPanel" style="display: none;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-clipboard-check"></i> File Validation Results</h5>
</div>
<div class="card-body" id="adminValidationResults">
<!-- Validation results will be shown here -->
</div>
</div>
<!-- Import Progress Panel -->
<div class="card mb-4" id="adminProgressPanel" style="display: none;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-hourglass-split"></i> Import Progress</h5>
</div>
<div class="card-body">
<div class="progress mb-3">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%" id="adminProgressBar">0%</div>
</div>
<div id="adminProgressStatus">Ready to import...</div>
</div>
</div>
<!-- Import Results Panel -->
<div class="card mb-4" id="adminResultsPanel" style="display: none;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-check-circle-fill"></i> Import Results</h5>
</div>
<div class="card-body" id="adminImportResults">
<!-- Import results will be shown here -->
</div>
</div>
<!-- Data Management Panel -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-database"></i> Data Management</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Clear Table Data</h6>
<p class="text-muted small">Remove all records from a specific table (cannot be undone)</p>
<div class="input-group">
<select class="form-select" id="adminClearTableType">
<option value="">Select table to clear...</option>
</select>
<button class="btn btn-danger" onclick="clearAdminTable()">
<i class="bi bi-trash"></i> Clear Table
</button>
</div>
</div>
<div class="col-md-6">
<h6>Quick Actions</h6>
<div class="d-grid gap-2">
<button class="btn btn-outline-info" onclick="viewImportLogs()">
<i class="bi bi-journal-text"></i> View Import Logs
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Backup Tab -->
<div class="tab-pane fade" id="backup" role="tabpanel">
<div class="row">
@@ -813,16 +954,51 @@ 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() {
// 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();
loadIssues();
loadIssueStats();
// Don't load issues and stats on initial load - only when tab is clicked
// Auto-refresh every 5 minutes
setInterval(loadSystemHealth, 300000);
@@ -832,6 +1008,13 @@ document.addEventListener('DOMContentLoaded', function() {
loadIssues();
loadIssueStats();
});
// Load import data when Import tab is clicked
document.getElementById('import-tab').addEventListener('shown.bs.tab', function() {
loadAvailableImportFiles();
loadImportStatus();
});
});
});
// System Health Functions
@@ -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();
// 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 = '<option value="">Select data type...</option>';
clearTableSelect.innerHTML = '<option value="">Select table to clear...</option>';
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 =
'<div class="alert alert-danger">Error loading import status: ' + error.message + '</div>';
}
}
function displayImportStatus(status) {
const container = document.getElementById('importStatus');
let html = '<div class="row">';
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 += '</div><div class="row mt-2">';
}
html += `
<div class="col-md-4">
<div class="card border-${statusClass}">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<small class="fw-bold">${fileType}</small><br>
<small class="text-muted">${info.table_name}</small>
</div>
<div class="text-end">
<i class="bi bi-${statusIcon} text-${statusClass}"></i><br>
<small class="fw-bold">${info.record_count || 0}</small>
</div>
</div>
${info.error ? `<div class="text-danger small mt-1">${info.error}</div>` : ''}
</div>
</div>
</div>
`;
});
html += '</div>';
html += `<div class="mt-3 text-center">
<strong>Total Records: ${totalRecords.toLocaleString()}</strong>
</div>`;
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 += `
<div class="alert alert-${statusClass}">
<i class="bi bi-${statusIcon}"></i>
File validation ${result.valid ? 'passed' : 'failed'}
</div>
`;
// Headers validation
html += '<h6>Column Headers</h6>';
if (result.headers.missing.length > 0) {
html += `<div class="alert alert-warning">
<strong>Missing columns:</strong> ${result.headers.missing.join(', ')}
</div>`;
}
if (result.headers.extra.length > 0) {
html += `<div class="alert alert-info">
<strong>Extra columns:</strong> ${result.headers.extra.join(', ')}
</div>`;
}
if (result.headers.missing.length === 0 && result.headers.extra.length === 0) {
html += '<div class="alert alert-success">All expected columns found</div>';
}
// Sample data
if (result.sample_data && result.sample_data.length > 0) {
html += '<h6>Sample Data (First 10 rows)</h6>';
html += '<div class="table-responsive">';
html += '<table class="table table-sm table-striped">';
html += '<thead><tr>';
Object.keys(result.sample_data[0]).forEach(header => {
html += `<th>${header}</th>`;
});
html += '</tr></thead><tbody>';
result.sample_data.forEach(row => {
html += '<tr>';
Object.values(row).forEach(value => {
html += `<td class="small">${value || ''}</td>`;
});
html += '</tr>';
});
html += '</tbody></table></div>';
}
// Validation errors
if (result.validation_errors && result.validation_errors.length > 0) {
html += '<h6>Data Issues Found</h6>';
html += '<div class="alert alert-warning">';
result.validation_errors.forEach(error => {
html += `<div>Row ${error.row}, Field "${error.field}": ${error.error}</div>`;
});
if (result.total_errors > result.validation_errors.length) {
html += `<div class="mt-2"><strong>... and ${result.total_errors - result.validation_errors.length} more errors</strong></div>`;
}
html += '</div>';
}
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 = `
<div class="alert alert-${successClass}">
<h6><i class="bi bi-check-circle"></i> Import Completed</h6>
<p class="mb-0">
<strong>File Type:</strong> ${result.file_type}<br>
<strong>Records Imported:</strong> ${result.imported_count}<br>
<strong>Errors:</strong> ${result.total_errors || 0}
</p>
</div>
`;
if (result.errors && result.errors.length > 0) {
html += '<h6>Import Errors</h6>';
html += '<div class="alert alert-danger">';
result.errors.forEach(error => {
html += `<div><strong>Row ${error.row}:</strong> ${error.error}</div>`;
});
if (result.total_errors > result.errors.length) {
html += `<div class="mt-2"><strong>... and ${result.total_errors - result.errors.length} more errors</strong></div>`;
}
html += '</div>';
}
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 =>

View File

@@ -99,8 +99,8 @@
<i class="bi bi-person-circle"></i> User
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin" data-shortcut="Alt+A"><i class="bi bi-gear"></i> Admin <small>(Alt+A)</small></a></li>
<li><hr class="dropdown-divider"></li>
<li id="admin-menu-item" style="display: none;"><a class="dropdown-item" href="/admin" data-shortcut="Alt+A"><i class="bi bi-gear"></i> Admin <small>(Alt+A)</small></a></li>
<li id="admin-menu-divider" style="display: none;"><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="logout()"><i class="bi bi-box-arrow-right"></i> Logout</a></li>
</ul>
</li>
@@ -122,7 +122,7 @@
<div class="row align-items-center">
<div class="col-md-6">
<small class="text-muted">
&copy; 2024 Delphi Consulting Group Database System
&copy; <span id="currentYear"></span> Delphi Consulting Group Database System
<span class="mx-2">|</span>
<span id="currentPageDisplay">Loading...</span>
</small>
@@ -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 = `<i class="bi bi-person-circle"></i> ${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