@@ -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 += `| ${header} | `;
+ });
+ html += '
';
+
+ result.sample_data.forEach(row => {
+ html += '';
+ Object.values(row).forEach(value => {
+ html += `| ${value || ''} | `;
+ });
+ html += '
';
+ });
+ html += '
';
+ }
+
+ // 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