770 lines
39 KiB
HTML
770 lines
39 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}{{ title }}{% endblock %}</title>
|
|
|
|
<!-- Icons (Font Awesome) -->
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
|
|
<!-- Tailwind CSS -->
|
|
<!-- Custom Tailwind CSS -->
|
|
<link href="/static/css/tailwind.css" rel="stylesheet">
|
|
|
|
{% block bridge_css %}{% endblock %}
|
|
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body class="flex flex-col min-h-screen bg-neutral-50 dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 antialiased">
|
|
<!-- Navigation -->
|
|
<nav class="bg-primary-600 border-b border-primary-700 shadow-sm">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center h-16">
|
|
<!-- Brand -->
|
|
<div class="flex items-center">
|
|
<a href="/" class="text-white font-semibold text-xl hover:text-primary-100 transition-colors">
|
|
Delphi Database System
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Desktop Navigation -->
|
|
<div class="hidden md:flex items-center space-x-1">
|
|
<a href="/customers" data-shortcut="Alt+C" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-users"></i>
|
|
<span>Customers</span>
|
|
</a>
|
|
<a href="/files" data-shortcut="Alt+F" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-folder"></i>
|
|
<span>Files</span>
|
|
</a>
|
|
<a href="/financial" data-shortcut="Alt+L" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-calculator"></i>
|
|
<span>Ledger</span>
|
|
</a>
|
|
<a href="/documents" data-shortcut="Alt+D" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-file-lines"></i>
|
|
<span>Documents</span>
|
|
</a>
|
|
<a href="/search" data-shortcut="Ctrl+F" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-magnifying-glass"></i>
|
|
<span>Search</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Right side items -->
|
|
<div class="flex items-center space-x-3">
|
|
<!-- Theme Toggle -->
|
|
<button onclick="toggleTheme()" title="Toggle dark mode" class="flex items-center justify-center w-10 h-10 bg-primary-700 hover:bg-primary-800 text-white rounded-lg transition-colors duration-200">
|
|
<i class="fas fa-sun dark:hidden text-sm"></i>
|
|
<i class="fas fa-moon hidden dark:block text-sm"></i>
|
|
</button>
|
|
|
|
<!-- User Dropdown -->
|
|
<div class="relative" id="userDropdown">
|
|
<button onclick="toggleUserMenu()" class="flex items-center gap-2 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-circle-user"></i>
|
|
<span>User</span>
|
|
<i class="fa-solid fa-chevron-down text-xs"></i>
|
|
</button>
|
|
<div id="userMenu" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 z-50">
|
|
<div class="py-1">
|
|
<a id="admin-menu-item" href="/admin" class="hidden flex items-center gap-2 px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors">
|
|
<i class="fa-solid fa-gear"></i>
|
|
<span>Admin</span>
|
|
</a>
|
|
<div id="admin-menu-divider" class="hidden border-t border-neutral-200 dark:border-neutral-700 my-1"></div>
|
|
<a href="#" onclick="logout()" class="flex items-center gap-2 px-4 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors">
|
|
<i class="fa-solid fa-right-from-bracket"></i>
|
|
<span>Logout</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile menu button -->
|
|
<button onclick="toggleMobileMenu()" class="md:hidden flex items-center justify-center w-10 h-10 bg-primary-700 hover:bg-primary-800 text-white rounded-lg transition-colors">
|
|
<i id="mobileMenuIcon" class="fa-solid fa-bars"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile Navigation -->
|
|
<div id="mobileMenu" class="hidden md:hidden border-t border-primary-700 pt-4 pb-4">
|
|
<div class="space-y-1">
|
|
<a href="/customers" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-users"></i>
|
|
<span>Customers</span>
|
|
</a>
|
|
<a href="/files" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-folder"></i>
|
|
<span>Files</span>
|
|
</a>
|
|
<a href="/financial" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-calculator"></i>
|
|
<span>Ledger</span>
|
|
</a>
|
|
<a href="/documents" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-file-lines"></i>
|
|
<span>Documents</span>
|
|
</a>
|
|
<a href="/search" class="flex items-center gap-3 px-3 py-2 rounded-lg text-primary-100 hover:text-white hover:bg-primary-700 transition-all duration-200">
|
|
<i class="fa-solid fa-magnifying-glass"></i>
|
|
<span>Search</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex-grow">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="mt-auto border-t border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
|
© <span id="currentYear"></span> Delphi Consulting Group Database System
|
|
<span class="mx-2">|</span>
|
|
<span id="currentPageDisplay">Loading...</span>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<button type="button" onclick="openSupportModal()" class="bg-primary-600 text-white hover:bg-primary-700 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors duration-200 flex items-center gap-2">
|
|
<i class="fas fa-bug"></i>
|
|
<span>Report Issue</span>
|
|
</button>
|
|
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
|
Found a bug?
|
|
<button onclick="openSupportModal()" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors duration-200 underline">
|
|
Report Issue
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Include Support Modal -->
|
|
{% include 'support_modal.html' %}
|
|
|
|
<!-- Keyboard Shortcuts Help Modal -->
|
|
<div id="shortcutsModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full max-h-screen overflow-hidden">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h2 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
|
<i class="fa-solid fa-keyboard"></i>
|
|
<span>Keyboard Shortcuts</span>
|
|
</h2>
|
|
<button onclick="closeShortcutsModal()" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors">
|
|
<i class="fa-solid fa-xmark text-xl"></i>
|
|
</button>
|
|
</div>
|
|
<div class="px-6 py-4 max-h-96 overflow-y-auto scrollbar-thin">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
|
|
<i class="fa-solid fa-house"></i>
|
|
<span>Navigation</span>
|
|
</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Customers/Rolodex</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+C</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">File Cabinet</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+F</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Ledger/Financial</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+L</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Documents/QDROs</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+D</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Admin Panel</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+A</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Global Search</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Ctrl+F</kbd>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
|
|
<i class="fa-solid fa-pencil"></i>
|
|
<span>Forms</span>
|
|
</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">New Record</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Ctrl+N</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Save</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Ctrl+S</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Edit Mode</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F9</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Complete/Save</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F2</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Clear/Cancel</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F8</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Delete Record</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Del</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Cancel/Close</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Esc</kbd>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
|
|
<i class="fa-solid fa-list"></i>
|
|
<span>Lists/Tables</span>
|
|
</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Navigate records</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">↑/↓</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Page navigation</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Page Up/Down</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">First/Last record</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Home/End</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Open/Edit record</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Enter</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Change dates</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">+/-</kbd>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="flex items-center gap-2 text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-3">
|
|
<i class="fa-solid fa-screwdriver-wrench"></i>
|
|
<span>System</span>
|
|
</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Help (this dialog)</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F1</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Menu</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">F10</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Memo/Notes</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+M</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Time Tracker</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+T</kbd>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Balance Summary</span>
|
|
<kbd class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded text-xs font-mono">Alt+B</kbd>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
|
|
<button onclick="closeShortcutsModal()" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors duration-200">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Navigation JS -->
|
|
<script>
|
|
function toggleUserMenu() {
|
|
const menu = document.getElementById('userMenu');
|
|
menu.classList.toggle('hidden');
|
|
}
|
|
|
|
function toggleMobileMenu() {
|
|
const menu = document.getElementById('mobileMenu');
|
|
const icon = document.getElementById('mobileMenuIcon');
|
|
menu.classList.toggle('hidden');
|
|
icon.classList.toggle('fa-bars');
|
|
icon.classList.toggle('fa-xmark');
|
|
}
|
|
|
|
function openShortcutsModal() {
|
|
document.getElementById('shortcutsModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeShortcutsModal() {
|
|
document.getElementById('shortcutsModal').classList.add('hidden');
|
|
}
|
|
|
|
// Close menus when clicking outside
|
|
document.addEventListener('click', function(event) {
|
|
const dropdown = document.getElementById('userDropdown');
|
|
const menu = document.getElementById('userMenu');
|
|
const shortcutsModal = document.getElementById('shortcutsModal');
|
|
|
|
if (!dropdown.contains(event.target)) {
|
|
menu.classList.add('hidden');
|
|
}
|
|
|
|
// Close shortcuts modal when clicking outside
|
|
if (event.target === shortcutsModal) {
|
|
closeShortcutsModal();
|
|
}
|
|
});
|
|
|
|
// Handle escape key for modal
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape') {
|
|
closeShortcutsModal();
|
|
}
|
|
});
|
|
</script>
|
|
<script>
|
|
// Global modal helpers for Tailwind-based modals
|
|
function openModal(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.classList.remove('hidden');
|
|
}
|
|
function closeModal(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.classList.add('hidden');
|
|
}
|
|
</script>
|
|
|
|
<!-- Custom JavaScript -->
|
|
<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
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeKeyboardShortcuts();
|
|
updateCurrentPageDisplay();
|
|
updateCurrentYear();
|
|
initializeAuthManager();
|
|
checkUserPermissions();
|
|
});
|
|
|
|
// Update current page display in footer
|
|
function updateCurrentPageDisplay() {
|
|
const path = window.location.pathname;
|
|
const pageNames = {
|
|
'/': 'Dashboard',
|
|
'/login': 'Login',
|
|
'/customers': 'Customer Management',
|
|
'/files': 'File Cabinet',
|
|
'/financial': 'Financial/Ledger',
|
|
'/documents': 'Document Management',
|
|
'/import': 'Data Import',
|
|
'/search': 'Advanced Search',
|
|
'/admin': 'System Administration'
|
|
};
|
|
|
|
const currentPage = pageNames[path] || `Page: ${path}`;
|
|
const displayElement = document.getElementById('currentPageDisplay');
|
|
if (displayElement) {
|
|
displayElement.textContent = `Current: ${currentPage}`;
|
|
}
|
|
}
|
|
|
|
// 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').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
|
|
};
|
|
</script>
|
|
</body>
|
|
</html> |