This commit is contained in:
HotSwapp
2025-08-09 16:37:57 -05:00
parent 5f74243c8c
commit c2f3c4411d
35 changed files with 9209 additions and 4633 deletions

View File

@@ -5,135 +5,146 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ title }}{% endblock %}</title>
<!-- Bootstrap 5.3 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 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">
<!-- Custom CSS -->
<link href="/static/css/main.css" rel="stylesheet">
<link href="/static/css/themes.css" rel="stylesheet">
<link href="/static/css/components.css" rel="stylesheet">
{% block bridge_css %}{% endblock %}
<style>
/* Footer Enhancements */
footer .btn-outline-primary:hover {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
transform: translateY(-1px);
transition: all 0.2s ease;
}
footer .text-primary:hover {
color: #0056b3 !important;
transition: color 0.2s ease;
}
footer small {
color: #6c757d !important;
}
#currentPageDisplay {
color: #495057 !important;
font-weight: 500;
}
/* Responsive footer adjustments */
@media (max-width: 768px) {
footer .row {
text-align: center !important;
}
footer .col-md-6:first-child {
margin-bottom: 0.5rem;
}
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body class="d-flex flex-column min-vh-100">
<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="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
Delphi Database System
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/customers" data-shortcut="Alt+C">
<i class="bi bi-people"></i> Customers <small>(Alt+C)</small>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/files" data-shortcut="Alt+F">
<i class="bi bi-folder"></i> Files <small>(Alt+F)</small>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/financial" data-shortcut="Alt+L">
<i class="bi bi-calculator"></i> Ledger <small>(Alt+L)</small>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/documents" data-shortcut="Alt+D">
<i class="bi bi-file-text"></i> Documents <small>(Alt+D)</small>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/search" data-shortcut="Ctrl+F">
<i class="bi bi-search"></i> Search <small>(Ctrl+F)</small>
</a>
</li>
</ul>
<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>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> User
</a>
<ul class="dropdown-menu">
<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>
</ul>
<!-- 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-1">
<div class="container-fluid mt-3 mb-4">
<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 py-3 border-top shadow-sm" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-color: #dee2e6 !important;">
<div class="container">
<div class="row align-items-center">
<div class="col-md-6">
<small class="text-muted">
&copy; <span id="currentYear"></span> Delphi Consulting Group Database System
<span class="mx-2">|</span>
<span id="currentPageDisplay">Loading...</span>
</small>
<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">
&copy; <span id="currentYear"></span> Delphi Consulting Group Database System
<span class="mx-2">|</span>
<span id="currentPageDisplay">Loading...</span>
</div>
<div class="col-md-6 text-end">
<button type="button" class="btn btn-outline-primary btn-sm me-3" onclick="openSupportModal()">
<i class="fas fa-bug me-1"></i>Report Issue
<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>
<small class="text-muted">
Found a bug? <a href="#" onclick="openSupportModal()" class="text-primary text-decoration-none">Report Issue</a>
</small>
<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>
@@ -143,71 +154,220 @@
{% include 'support_modal.html' %}
<!-- Keyboard Shortcuts Help Modal -->
<div class="modal fade" id="shortcutsModal" tabindex="-1" aria-labelledby="shortcutsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="shortcutsModalLabel">
<i class="bi bi-keyboard"></i> Keyboard Shortcuts
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6><i class="bi bi-house"></i> Navigation</h6>
<ul class="list-unstyled">
<li><kbd>Alt+C</kbd> - Customers/Rolodex</li>
<li><kbd>Alt+F</kbd> - File Cabinet</li>
<li><kbd>Alt+L</kbd> - Ledger/Financial</li>
<li><kbd>Alt+D</kbd> - Documents/QDROs</li>
<li><kbd>Alt+A</kbd> - Admin Panel</li>
<li><kbd>Ctrl+F</kbd> - Global Search</li>
</ul>
<h6><i class="bi bi-pencil"></i> Forms</h6>
<ul class="list-unstyled">
<li><kbd>Ctrl+N</kbd> - New Record</li>
<li><kbd>Ctrl+S</kbd> - Save</li>
<li><kbd>F9</kbd> - Edit Mode</li>
<li><kbd>F2</kbd> - Complete/Save</li>
<li><kbd>F8</kbd> - Clear/Cancel</li>
<li><kbd>Del</kbd> - Delete Record</li>
<li><kbd>Esc</kbd> - Cancel/Close</li>
<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 class="col-md-6">
<h6><i class="bi bi-list"></i> Lists/Tables</h6>
<ul class="list-unstyled">
<li><kbd>↑/↓</kbd> - Navigate records</li>
<li><kbd>Page Up/Down</kbd> - Page navigation</li>
<li><kbd>Home/End</kbd> - First/Last record</li>
<li><kbd>Enter</kbd> - Open/Edit record</li>
<li><kbd>+/-</kbd> - Change dates</li>
<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>
<h6><i class="bi bi-tools"></i> System</h6>
<ul class="list-unstyled">
<li><kbd>F1</kbd> - Help (this dialog)</li>
<li><kbd>F10</kbd> - Menu</li>
<li><kbd>Alt+M</kbd> - Memo/Notes</li>
<li><kbd>Alt+T</kbd> - Time Tracker</li>
<li><kbd>Alt+B</kbd> - Balance Summary</li>
</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 class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</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>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 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>
@@ -272,14 +432,14 @@
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';
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('.nav-link.dropdown-toggle');
const userDropdown = document.querySelector('#userDropdown button span');
if (user.full_name && userDropdown) {
userDropdown.innerHTML = `<i class="bi bi-person-circle"></i> ${user.full_name}`;
userDropdown.textContent = user.full_name;
}
}
} catch (error) {
@@ -368,28 +528,136 @@
}
}
function setupActivityMonitoring() {
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();
});
});
// Check every 30 minutes if user has been inactive for more than 4 hours
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 fourHours = 4 * 60 * 60 * 1000;
if (now - lastActivity > fourHours) {
// User has been inactive for 4+ hours, logout
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');
}
}, 30 * 60 * 1000); // Check every 30 minutes
}, 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
@@ -402,12 +670,101 @@
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>