all working
This commit is contained in:
@@ -55,25 +55,25 @@
|
||||
|
||||
<!-- Main Navigation Tabs -->
|
||||
<ul id="adminTabs" class="flex border-b border-neutral-200 dark:border-neutral-700 mb-6" role="tablist">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors active" id="overview-tab" data-target="#overview" type="button" role="tab">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors active" id="overview-tab" data-tab-target="#overview" type="button" role="tab">
|
||||
<i class="fas fa-tachometer-alt"></i> Overview
|
||||
</li>
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="users-tab" data-target="#users" type="button" role="tab">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="users-tab" data-tab-target="#users" type="button" role="tab">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</li>
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="settings-tab" data-target="#settings" type="button" role="tab">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="settings-tab" data-tab-target="#settings" type="button" role="tab">
|
||||
<i class="fas fa-sliders-h"></i> System Settings
|
||||
</li>
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="maintenance-tab" data-target="#maintenance" type="button" role="tab">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="maintenance-tab" data-tab-target="#maintenance" type="button" role="tab">
|
||||
<i class="fas fa-tools"></i> Maintenance
|
||||
</li>
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="issues-tab" data-target="#issues" type="button" role="tab">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="issues-tab" data-tab-target="#issues" type="button" role="tab">
|
||||
<i class="fas fa-bug"></i> Issue Tracking
|
||||
</li>
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="import-tab" data-target="#import" type="button" role="tab">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="import-tab" data-tab-target="#import" type="button" role="tab">
|
||||
<i class="fa-solid fa-upload"></i> Data Import
|
||||
</li>
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="backup-tab" data-target="#backup" type="button" role="tab">
|
||||
<li class="px-4 py-2 -mb-px border-b-2 border-transparent hover:border-primary-600 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" id="backup-tab" data-tab-target="#backup" type="button" role="tab">
|
||||
<i class="fas fa-hdd"></i> Backup & Restore
|
||||
</li>
|
||||
</ul>
|
||||
@@ -951,7 +951,7 @@ function initializeTabs() {
|
||||
const panes = document.querySelectorAll('#adminTabContent > div[role="tabpanel"]');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const target = tab.getAttribute('data-target');
|
||||
const target = tab.getAttribute('data-tab-target');
|
||||
if (!target) return;
|
||||
// deactivate
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
@@ -996,7 +996,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSettings();
|
||||
loadLookupTables();
|
||||
loadBackups();
|
||||
// Tabs setup (no Bootstrap JS)
|
||||
// Tabs setup
|
||||
initializeTabs();
|
||||
|
||||
// Auto-refresh every 5 minutes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -347,12 +347,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Handle escape key for modal
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeShortcutsModal();
|
||||
}
|
||||
});
|
||||
// Escape handling is centralized in keyboard-shortcuts.js
|
||||
</script>
|
||||
<script>
|
||||
// Global modal helpers for Tailwind-based modals
|
||||
@@ -367,20 +362,18 @@
|
||||
</script>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<!-- Load main.js first so global handlers are registered before other scripts -->
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/alerts.js"></script>
|
||||
<script src="/static/js/keyboard-shortcuts.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
|
||||
<script>
|
||||
// Initialize keyboard shortcuts on page load
|
||||
// Initialize page meta on load (keyboard shortcuts initialized centrally in main.js)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeKeyboardShortcuts();
|
||||
updateCurrentPageDisplay();
|
||||
updateCurrentYear();
|
||||
initializeAuthManager();
|
||||
checkUserPermissions();
|
||||
});
|
||||
|
||||
// Update current page display in footer
|
||||
@@ -414,357 +407,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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').classList.remove('hidden');
|
||||
document.getElementById('admin-menu-divider').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update user display name if available
|
||||
const userDropdown = document.querySelector('#userDropdown button span');
|
||||
if (user.full_name && userDropdown) {
|
||||
userDropdown.textContent = 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
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token && !isLoginPage()) {
|
||||
// Verify token is still valid
|
||||
checkTokenValidity();
|
||||
|
||||
// Set up periodic token refresh (every hour)
|
||||
setInterval(refreshTokenIfNeeded, 3600000); // 1 hour
|
||||
|
||||
// Set up activity monitoring for auto-refresh
|
||||
setupActivityMonitoring();
|
||||
} else if (!isLoginPage() && !token) {
|
||||
// No token and not on login page - redirect to login
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
function isLoginPage() {
|
||||
return window.location.pathname === '/login' || window.location.pathname === '/';
|
||||
}
|
||||
|
||||
async function checkTokenValidity() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) return false;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Token is invalid, remove it and redirect to login
|
||||
localStorage.removeItem('auth_token');
|
||||
if (!isLoginPage()) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking token validity:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTokenIfNeeded() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
// Try to get a new token
|
||||
const response = await fetch('/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
console.log('Token refreshed successfully');
|
||||
} else {
|
||||
// If refresh fails, check if current token is still valid
|
||||
const isValid = await checkTokenValidity();
|
||||
if (!isValid) {
|
||||
localStorage.removeItem('auth_token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupActivityMonitoring() {
|
||||
let lastActivity = Date.now();
|
||||
let warningShown = false;
|
||||
let inactivityWarningMinutes = 240; // default 4 hours
|
||||
const inactivityGraceMinutes = 5; // auto-logout after warning + 5 minutes
|
||||
let inactivityAlertEl = null;
|
||||
|
||||
try {
|
||||
const minutes = await getInactivityWarningMinutes();
|
||||
if (Number.isFinite(minutes) && minutes > 0) {
|
||||
inactivityWarningMinutes = minutes;
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Using default inactivity warning minutes');
|
||||
}
|
||||
|
||||
// Track user activity
|
||||
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
|
||||
activityEvents.forEach(event => {
|
||||
document.addEventListener(event, () => {
|
||||
lastActivity = Date.now();
|
||||
warningShown = false; // Reset warning flag on activity
|
||||
hideInactivityWarning();
|
||||
});
|
||||
});
|
||||
|
||||
function showInactivityWarning() {
|
||||
hideInactivityWarning();
|
||||
|
||||
const msg = `You've been inactive. Your session may expire due to inactivity.`;
|
||||
if (window.alerts && typeof window.alerts.show === 'function') {
|
||||
inactivityAlertEl = window.alerts.show(msg, 'warning', {
|
||||
title: 'Session Warning',
|
||||
html: false,
|
||||
duration: 0,
|
||||
dismissible: true,
|
||||
id: 'inactivity-warning',
|
||||
actions: [
|
||||
{
|
||||
label: 'Stay Logged In',
|
||||
classes: 'bg-warning-600 hover:bg-warning-700 text-white text-xs px-3 py-1 rounded',
|
||||
onClick: () => extendSession(),
|
||||
autoClose: true
|
||||
},
|
||||
{
|
||||
label: 'Dismiss',
|
||||
classes: 'bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200 text-xs px-3 py-1 rounded',
|
||||
onClick: () => hideInactivityWarning(),
|
||||
autoClose: true
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
alert('Session Warning: ' + msg);
|
||||
}
|
||||
|
||||
// Auto-hide after 2 minutes if no action taken
|
||||
setTimeout(() => {
|
||||
hideInactivityWarning();
|
||||
}, 2 * 60 * 1000);
|
||||
}
|
||||
|
||||
function hideInactivityWarning() {
|
||||
const el = document.getElementById('inactivity-warning');
|
||||
if (el && el.remove) el.remove();
|
||||
inactivityAlertEl = null;
|
||||
}
|
||||
|
||||
function extendSession() {
|
||||
refreshTokenIfNeeded();
|
||||
hideInactivityWarning();
|
||||
showSessionExtendedNotification();
|
||||
}
|
||||
|
||||
// Check every 5 minutes for inactivity
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const warningMs = inactivityWarningMinutes * 60 * 1000;
|
||||
const logoutMs = (inactivityWarningMinutes + inactivityGraceMinutes) * 60 * 1000;
|
||||
const timeSinceActivity = now - lastActivity;
|
||||
|
||||
if (timeSinceActivity > warningMs && !warningShown) {
|
||||
showInactivityWarning();
|
||||
warningShown = true;
|
||||
}
|
||||
|
||||
if (timeSinceActivity > logoutMs) {
|
||||
logout('Session expired due to inactivity');
|
||||
}
|
||||
}, 5 * 60 * 1000); // Check every 5 minutes
|
||||
}
|
||||
|
||||
async function getInactivityWarningMinutes() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) return 240;
|
||||
const resp = await fetch('/api/settings/inactivity_warning_minutes', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!resp.ok) return 240;
|
||||
const data = await resp.json();
|
||||
if (typeof data.minutes === 'number') return data.minutes;
|
||||
const parsed = parseInt(data.setting_value || data.minutes, 10);
|
||||
return Number.isFinite(parsed) ? parsed : 240;
|
||||
}
|
||||
|
||||
function showSessionExtendedNotification() {
|
||||
if (window.alerts && typeof window.alerts.success === 'function') {
|
||||
window.alerts.success('Your session has been refreshed successfully.', {
|
||||
title: 'Session Extended',
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Fallback
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'fixed top-4 right-4 bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded-lg shadow-lg z-50 max-w-sm';
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-check-circle text-green-500"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium">Session Extended</p>
|
||||
<p class="text-xs mt-1">Your session has been refreshed successfully.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(notification);
|
||||
setTimeout(() => notification.remove(), 3000);
|
||||
}
|
||||
|
||||
// Enhanced logout function
|
||||
function logout(reason = null) {
|
||||
localStorage.removeItem('auth_token');
|
||||
if (reason) {
|
||||
// Store logout reason to show on login page
|
||||
sessionStorage.setItem('logout_reason', reason);
|
||||
}
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// Theme Management
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const isDark = html.classList.contains('dark');
|
||||
|
||||
if (isDark) {
|
||||
html.classList.remove('dark');
|
||||
localStorage.setItem('theme-preference', 'light');
|
||||
saveThemePreference('light');
|
||||
} else {
|
||||
html.classList.add('dark');
|
||||
localStorage.setItem('theme-preference', 'dark');
|
||||
saveThemePreference('dark');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTheme() {
|
||||
// Check for saved theme preference
|
||||
const savedTheme = localStorage.getItem('theme-preference');
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Use saved theme, or default to system preference
|
||||
const theme = savedTheme || (prefersDark ? 'dark' : 'light');
|
||||
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Load user's theme preference from server if authenticated
|
||||
loadUserThemePreference();
|
||||
}
|
||||
|
||||
async function saveThemePreference(theme) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token || isLoginPage()) return;
|
||||
|
||||
try {
|
||||
await fetch('/api/auth/theme-preference', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ theme_preference: theme })
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Could not save theme preference to server:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserThemePreference() {
|
||||
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.theme_preference) {
|
||||
if (user.theme_preference === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
localStorage.setItem('theme-preference', user.theme_preference);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not load theme preference from server:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme before other scripts
|
||||
initializeTheme();
|
||||
|
||||
// Make functions globally available
|
||||
window.authManager = {
|
||||
checkTokenValidity,
|
||||
refreshTokenIfNeeded,
|
||||
logout
|
||||
};
|
||||
|
||||
window.themeManager = {
|
||||
toggleTheme,
|
||||
initializeTheme,
|
||||
saveThemePreference,
|
||||
loadUserThemePreference
|
||||
};
|
||||
// Auth helpers and theme management are centralized in /static/js/main.js
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -435,25 +435,51 @@
|
||||
<script>
|
||||
// Document Management JavaScript
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize page
|
||||
loadTemplates();
|
||||
loadQdros();
|
||||
loadCategories();
|
||||
// Check authentication first
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up keyboard shortcuts
|
||||
setupKeyboardShortcuts();
|
||||
|
||||
// Set up event handlers
|
||||
setupEventHandlers();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(function() {
|
||||
if (document.querySelector('#templates-tab').classList.contains('active')) {
|
||||
loadTemplates();
|
||||
} else if (document.querySelector('#qdros-tab').classList.contains('active')) {
|
||||
loadQdros();
|
||||
// Wait for API helpers to be ready, then initialize
|
||||
function initializeDocuments() {
|
||||
if (typeof apiGet === 'function') {
|
||||
// Ensure API headers are set up with token
|
||||
if (window.apiHeaders && token) {
|
||||
window.apiHeaders['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Initialize the first tab as active
|
||||
document.getElementById('templates-tab').click();
|
||||
|
||||
// Load initial data
|
||||
loadCategories();
|
||||
|
||||
// Set up keyboard shortcuts
|
||||
setupKeyboardShortcuts();
|
||||
|
||||
// Set up event handlers
|
||||
setupEventHandlers();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(function() {
|
||||
const templatesTab = document.querySelector('#templates-tab');
|
||||
const qdrosTab = document.querySelector('#qdros-tab');
|
||||
|
||||
if (templatesTab.classList.contains('text-blue-500')) {
|
||||
loadTemplates();
|
||||
} else if (qdrosTab.classList.contains('text-blue-500')) {
|
||||
loadQdros();
|
||||
}
|
||||
}, 30000);
|
||||
} else {
|
||||
// API helpers not ready yet, try again in 100ms
|
||||
setTimeout(initializeDocuments, 100);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
initializeDocuments();
|
||||
});
|
||||
|
||||
function setupKeyboardShortcuts() {
|
||||
@@ -568,8 +594,18 @@ function setupEventHandlers() {
|
||||
document.getElementById('refreshQdrosBtn').addEventListener('click', loadQdros);
|
||||
}
|
||||
|
||||
// Helper function for authenticated API calls
|
||||
function getAuthHeaders() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
return {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
console.log('🔍 DEBUG: loadTemplates() called');
|
||||
const search = document.getElementById('templateSearch').value;
|
||||
const category = document.getElementById('categoryFilter').value;
|
||||
|
||||
@@ -577,14 +613,34 @@ async function loadTemplates() {
|
||||
if (search) url += `search=${encodeURIComponent(search)}&`;
|
||||
if (category) url += `category=${encodeURIComponent(category)}&`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to load templates');
|
||||
// Ensure we have auth token for this API call
|
||||
const token = localStorage.getItem('auth_token');
|
||||
console.log('🔍 DEBUG: Token exists:', !!token, 'Length:', token?.length);
|
||||
if (!token) {
|
||||
console.log('🔍 DEBUG: No token found, redirecting to login');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 DEBUG: Making API call to:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
console.log('🔍 DEBUG: Response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const templates = await response.json();
|
||||
console.log('🔍 DEBUG: Templates loaded:', templates.length, 'items');
|
||||
displayTemplates(templates);
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
showAlert('Error loading templates: ' + error.message, 'danger');
|
||||
console.error('🔍 DEBUG: Error in loadTemplates:', error);
|
||||
try { logClientError({ message: 'Error loading templates', action: 'loadTemplates', error }); } catch (_) {}
|
||||
showAlert('Error loading templates: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,14 +690,27 @@ async function loadQdros() {
|
||||
if (search) url += `search=${encodeURIComponent(search)}&`;
|
||||
if (status) url += `status_filter=${encodeURIComponent(status)}&`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to load QDROs');
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const qdros = await response.json();
|
||||
displayQdros(qdros);
|
||||
} catch (error) {
|
||||
console.error('Error loading QDROs:', error);
|
||||
showAlert('Error loading QDROs: ' + error.message, 'danger');
|
||||
try { logClientError({ message: 'Error loading QDROs', action: 'loadQdros', error }); } catch (_) {}
|
||||
showAlert('Error loading QDROs: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,10 +762,23 @@ function getStatusBadgeClass(status) {
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const response = await fetch('/api/documents/categories/');
|
||||
if (!response.ok) throw new Error('Failed to load categories');
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/documents/categories/', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const categories = await response.json();
|
||||
|
||||
const select = document.getElementById('categoryFilter');
|
||||
const templateSelect = document.getElementById('templateCategory');
|
||||
|
||||
@@ -711,6 +793,7 @@ async function loadCategories() {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading categories:', error);
|
||||
try { logClientError({ message: 'Error loading categories', action: 'loadCategories', error }); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,7 +819,9 @@ function openTemplateModal(templateId = null) {
|
||||
|
||||
async function loadTemplateForEditing(templateId) {
|
||||
try {
|
||||
const response = await fetch(`/api/documents/templates/${templateId}`);
|
||||
const response = await fetch(`/api/documents/templates/${templateId}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to load template');
|
||||
|
||||
const template = await response.json();
|
||||
@@ -750,7 +835,8 @@ async function loadTemplateForEditing(templateId) {
|
||||
updateVariableCount();
|
||||
} catch (error) {
|
||||
console.error('Error loading template:', error);
|
||||
showAlert('Error loading template: ' + error.message, 'danger');
|
||||
try { logClientError({ message: 'Error loading template for edit', action: 'loadTemplateForEditing', error, extra: { templateId } }); } catch (_) {}
|
||||
showAlert('Error loading template: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,7 +874,8 @@ async function saveTemplate() {
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error saving template:', error);
|
||||
showAlert('Error saving template: ' + error.message, 'danger');
|
||||
try { logClientError({ message: 'Error saving template', action: 'saveTemplate', error }); } catch (_) {}
|
||||
showAlert('Error saving template: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,7 +901,9 @@ function extractVariables(content) {
|
||||
|
||||
async function loadDocumentStats() {
|
||||
try {
|
||||
const response = await fetch('/api/documents/stats/summary');
|
||||
const response = await fetch('/api/documents/stats/summary', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to load statistics');
|
||||
|
||||
const stats = await response.json();
|
||||
@@ -855,7 +944,8 @@ async function loadDocumentStats() {
|
||||
openModal('statsModal');
|
||||
} catch (error) {
|
||||
console.error('Error loading statistics:', error);
|
||||
showAlert('Error loading statistics: ' + error.message, 'danger');
|
||||
try { logClientError({ message: 'Error loading statistics', action: 'loadDocumentStats', error }); } catch (_) {}
|
||||
showAlert('Error loading statistics: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,6 +972,27 @@ function showAlert(message, type = 'info') {
|
||||
}
|
||||
}
|
||||
|
||||
// Lightweight client error logger specific to Documents page
|
||||
async function logClientError({ message, action = null, error = null, extra = null }) {
|
||||
try {
|
||||
const payload = {
|
||||
message: String(message || (error && error.message) || 'Unknown error'),
|
||||
action,
|
||||
stack: error && error.stack ? String(error.stack) : null,
|
||||
url: window.location.href,
|
||||
user_agent: navigator.userAgent,
|
||||
extra
|
||||
};
|
||||
await fetch('/api/documents/client-error', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
} catch (_) {
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder functions for additional features
|
||||
async function editTemplate(templateId) {
|
||||
openTemplateModal(templateId);
|
||||
@@ -900,7 +1011,8 @@ async function deleteTemplate(templateId) {
|
||||
if (confirm('Are you sure you want to delete this template?')) {
|
||||
try {
|
||||
const response = await fetch(`/api/documents/templates/${templateId}`, {
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to delete template');
|
||||
@@ -909,7 +1021,8 @@ async function deleteTemplate(templateId) {
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
console.error('Error deleting template:', error);
|
||||
showAlert('Error deleting template: ' + error.message, 'danger');
|
||||
try { logClientError({ message: 'Error deleting template', action: 'deleteTemplate', error, extra: { templateId } }); } catch (_) {}
|
||||
showAlert('Error deleting template: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -921,7 +1034,9 @@ function openGenerateModal() {
|
||||
|
||||
async function loadTemplatesForGeneration() {
|
||||
try {
|
||||
const response = await fetch('/api/documents/templates/');
|
||||
const response = await fetch('/api/documents/templates/', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to load templates');
|
||||
|
||||
const templates = await response.json();
|
||||
@@ -936,18 +1051,22 @@ async function loadTemplatesForGeneration() {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
try { logClientError({ message: 'Error loading templates for generation', action: 'loadTemplatesForGeneration', error }); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTemplatePreview(templateId) {
|
||||
try {
|
||||
const response = await fetch(`/api/documents/templates/${templateId}`);
|
||||
const response = await fetch(`/api/documents/templates/${templateId}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to load template');
|
||||
|
||||
const template = await response.json();
|
||||
document.getElementById('templatePreview').value = template.content;
|
||||
} catch (error) {
|
||||
console.error('Error loading template preview:', error);
|
||||
try { logClientError({ message: 'Error loading template preview', action: 'loadTemplatePreview', error, extra: { templateId } }); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -983,9 +1102,7 @@ async function generateDocument() {
|
||||
|
||||
const response = await fetch(`/api/documents/generate/${templateId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
@@ -1005,7 +1122,8 @@ async function generateDocument() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating document:', error);
|
||||
showAlert('Error generating document: ' + error.message, 'danger');
|
||||
try { logClientError({ message: 'Error generating document', action: 'generateDocument', error, extra: { templateId: (document.getElementById('generateTemplate')?.value || null) } }); } catch (_) {}
|
||||
showAlert('Error generating document: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,5 +1180,36 @@ async function deleteQdro(qdroId) {
|
||||
showAlert('QDRO delete functionality will be implemented', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// Tab navigation functionality
|
||||
function openTab(evt, tabName) {
|
||||
// Declare all variables
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
// Get all elements with class="tabcontent" and hide them
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].classList.add('hidden');
|
||||
}
|
||||
|
||||
// Get all elements with class="tablinks" and remove the "active" class
|
||||
tablinks = document.querySelectorAll('nav button');
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].classList.remove('border-blue-500', 'text-blue-500', 'dark:text-blue-400');
|
||||
tablinks[i].classList.add('border-transparent');
|
||||
}
|
||||
|
||||
// Show the current tab, and add an "active" class to the button that opened the tab
|
||||
document.getElementById(tabName).classList.remove('hidden');
|
||||
evt.currentTarget.classList.add('border-blue-500', 'text-blue-500', 'dark:text-blue-400');
|
||||
evt.currentTarget.classList.remove('border-transparent');
|
||||
|
||||
// Load data for the active tab
|
||||
if (tabName === 'templates') {
|
||||
loadTemplates();
|
||||
} else if (tabName === 'qdros') {
|
||||
loadQdros();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -124,8 +124,11 @@
|
||||
const data = await response.json();
|
||||
console.log('Login successful, token:', data.access_token);
|
||||
|
||||
// Store token
|
||||
// Store tokens
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
if (data.refresh_token) {
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showAlert('Login successful! Redirecting...', 'success');
|
||||
|
||||
Reference in New Issue
Block a user