413 lines
17 KiB
HTML
413 lines
17 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>
|
|
|
|
<!-- 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">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.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">
|
|
|
|
<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">
|
|
<!-- 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>
|
|
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex-grow-1">
|
|
<div class="container-fluid mt-3 mb-4">
|
|
{% 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">
|
|
© <span id="currentYear"></span> Delphi Consulting Group Database System
|
|
<span class="mx-2">|</span>
|
|
<span id="currentPageDisplay">Loading...</span>
|
|
</small>
|
|
</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
|
|
</button>
|
|
<small class="text-muted">
|
|
Found a bug? <a href="#" onclick="openSupportModal()" class="text-primary text-decoration-none">Report Issue</a>
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Include Support Modal -->
|
|
{% 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>
|
|
</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>
|
|
</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>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<!-- Custom JavaScript -->
|
|
<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').style.display = 'block';
|
|
document.getElementById('admin-menu-divider').style.display = 'block';
|
|
}
|
|
|
|
// Update user display name if available
|
|
const userDropdown = document.querySelector('.nav-link.dropdown-toggle');
|
|
if (user.full_name && userDropdown) {
|
|
userDropdown.innerHTML = `<i class="bi bi-person-circle"></i> ${user.full_name}`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking user permissions:', error);
|
|
}
|
|
}
|
|
|
|
// Authentication Manager
|
|
function initializeAuthManager() {
|
|
// Check if we have a valid token on page load
|
|
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);
|
|
}
|
|
}
|
|
|
|
function setupActivityMonitoring() {
|
|
let lastActivity = Date.now();
|
|
|
|
// Track user activity
|
|
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
|
|
|
|
activityEvents.forEach(event => {
|
|
document.addEventListener(event, () => {
|
|
lastActivity = Date.now();
|
|
});
|
|
});
|
|
|
|
// Check every 30 minutes if user has been inactive for more than 4 hours
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
const fourHours = 4 * 60 * 60 * 1000;
|
|
|
|
if (now - lastActivity > fourHours) {
|
|
// User has been inactive for 4+ hours, logout
|
|
logout('Session expired due to inactivity');
|
|
}
|
|
}, 30 * 60 * 1000); // Check every 30 minutes
|
|
}
|
|
|
|
// 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';
|
|
}
|
|
|
|
// Make functions globally available
|
|
window.authManager = {
|
|
checkTokenValidity,
|
|
refreshTokenIfNeeded,
|
|
logout
|
|
};
|
|
</script>
|
|
</body>
|
|
</html> |