705 Zeilen
26 KiB
HTML
705 Zeilen
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Admin Panel{% endblock %} - Lizenzverwaltung</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
|
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
|
{% block extra_css %}{% endblock %}
|
|
<style>
|
|
/* Global Status Colors */
|
|
:root {
|
|
--status-active: #28a745;
|
|
--status-warning: #ffc107;
|
|
--status-danger: #dc3545;
|
|
--status-inactive: #6c757d;
|
|
--status-info: #17a2b8;
|
|
--sidebar-width: 250px;
|
|
--sidebar-collapsed: 60px;
|
|
}
|
|
|
|
/* Status Classes - Global */
|
|
.status-aktiv { color: var(--status-active) !important; }
|
|
.status-ablaufend { color: var(--status-warning) !important; }
|
|
.status-abgelaufen { color: var(--status-danger) !important; }
|
|
.status-deaktiviert { color: var(--status-inactive) !important; }
|
|
|
|
/* Badge Variants */
|
|
.badge-aktiv { background-color: var(--status-active) !important; }
|
|
.badge-ablaufend { background-color: var(--status-warning) !important; color: #000 !important; }
|
|
.badge-abgelaufen { background-color: var(--status-danger) !important; }
|
|
.badge-deaktiviert { background-color: var(--status-inactive) !important; }
|
|
|
|
/* Session Timer Styles */
|
|
#session-timer {
|
|
font-family: monospace;
|
|
font-weight: bold;
|
|
font-size: 1.1rem;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.timer-normal {
|
|
background-color: var(--status-active);
|
|
color: white;
|
|
}
|
|
|
|
.timer-warning {
|
|
background-color: var(--status-warning);
|
|
color: #000;
|
|
}
|
|
|
|
.timer-danger {
|
|
background-color: var(--status-danger);
|
|
color: white;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
.timer-critical {
|
|
background-color: var(--status-danger);
|
|
color: white;
|
|
animation: blink 0.5s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 49% { opacity: 1; }
|
|
50%, 100% { opacity: 0; }
|
|
}
|
|
|
|
.session-warning-modal {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 9999;
|
|
max-width: 400px;
|
|
}
|
|
|
|
/* Table Improvements */
|
|
.table-container {
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
position: relative;
|
|
}
|
|
|
|
.table-sticky thead {
|
|
position: sticky;
|
|
top: 0;
|
|
background-color: #fff;
|
|
z-index: 10;
|
|
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.table-sticky thead th {
|
|
background-color: #f8f9fa;
|
|
border-bottom: 2px solid #dee2e6;
|
|
}
|
|
|
|
/* Inline Actions */
|
|
.btn-copy {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-copy:hover {
|
|
background-color: #e9ecef;
|
|
}
|
|
|
|
.btn-copy.copied {
|
|
background-color: var(--status-active);
|
|
color: white;
|
|
}
|
|
|
|
/* Toggle Switch */
|
|
.form-switch-custom {
|
|
display: inline-block;
|
|
}
|
|
|
|
.form-switch-custom .form-check-input {
|
|
cursor: pointer;
|
|
width: 3em;
|
|
height: 1.5em;
|
|
}
|
|
|
|
.form-switch-custom .form-check-input:checked {
|
|
background-color: var(--status-active);
|
|
border-color: var(--status-active);
|
|
}
|
|
|
|
/* Bulk Actions Bar */
|
|
.bulk-actions {
|
|
position: sticky;
|
|
bottom: 0;
|
|
background-color: #212529;
|
|
color: white;
|
|
padding: 1rem;
|
|
display: none;
|
|
z-index: 100;
|
|
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.bulk-actions.show {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
/* Checkbox Styling */
|
|
.checkbox-cell {
|
|
width: 40px;
|
|
}
|
|
|
|
.form-check-input-custom {
|
|
cursor: pointer;
|
|
width: 1.2em;
|
|
height: 1.2em;
|
|
}
|
|
|
|
/* Sortable Table Styles */
|
|
.sortable-table th.sortable {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
position: relative;
|
|
padding-right: 25px;
|
|
}
|
|
|
|
.sortable-table th.sortable:hover {
|
|
background-color: #e9ecef;
|
|
}
|
|
|
|
.sortable-table th.sortable::after {
|
|
content: '↕';
|
|
position: absolute;
|
|
right: 8px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.sortable-table th.sortable.asc::after {
|
|
content: '↑';
|
|
opacity: 1;
|
|
color: var(--status-active);
|
|
}
|
|
|
|
.sortable-table th.sortable.desc::after {
|
|
content: '↓';
|
|
opacity: 1;
|
|
color: var(--status-active);
|
|
}
|
|
|
|
/* Server-side sortable styles */
|
|
.server-sortable {
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.server-sortable:hover {
|
|
color: var(--bs-primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.sort-indicator {
|
|
margin-left: 5px;
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
.sort-indicator.active {
|
|
color: var(--bs-primary);
|
|
}
|
|
|
|
/* Sidebar Navigation */
|
|
.sidebar {
|
|
position: fixed;
|
|
top: 56px;
|
|
left: 0;
|
|
height: calc(100vh - 56px);
|
|
width: var(--sidebar-width);
|
|
background-color: #f8f9fa;
|
|
border-right: 1px solid #dee2e6;
|
|
overflow-y: auto;
|
|
transition: all 0.3s;
|
|
z-index: 100;
|
|
}
|
|
|
|
.sidebar.collapsed {
|
|
width: var(--sidebar-collapsed);
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 1rem;
|
|
background-color: #e9ecef;
|
|
border-bottom: 1px solid #dee2e6;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.sidebar-nav .nav-item {
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
|
|
.sidebar-nav .nav-link {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75rem 1rem;
|
|
color: #495057;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.sidebar-nav .nav-link:hover {
|
|
background-color: #e9ecef;
|
|
color: #212529;
|
|
}
|
|
|
|
.sidebar-nav .nav-link.active {
|
|
background-color: var(--bs-primary);
|
|
color: white;
|
|
}
|
|
|
|
.sidebar-nav .nav-link i {
|
|
margin-right: 0.5rem;
|
|
font-size: 1.2rem;
|
|
width: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.sidebar.collapsed .sidebar-nav .nav-link span {
|
|
display: none;
|
|
}
|
|
|
|
.sidebar-submenu {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
background-color: #f1f3f4;
|
|
max-height: none;
|
|
overflow: visible;
|
|
}
|
|
|
|
.sidebar-submenu .nav-link {
|
|
padding-left: 2.5rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Arrow indicator for items with submenus */
|
|
.nav-link.has-submenu::after {
|
|
content: '▾';
|
|
float: right;
|
|
opacity: 0.5;
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
/* Main Content with Sidebar */
|
|
.main-content {
|
|
margin-left: var(--sidebar-width);
|
|
transition: all 0.3s;
|
|
min-height: calc(100vh - 56px);
|
|
}
|
|
|
|
.main-content.expanded {
|
|
margin-left: var(--sidebar-collapsed);
|
|
}
|
|
|
|
|
|
|
|
/* Responsive Adjustments */
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
transform: translateX(-100%);
|
|
}
|
|
|
|
.sidebar.show {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.main-content {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-light">
|
|
<nav class="navbar navbar-dark bg-dark navbar-expand-lg sticky-top">
|
|
<div class="container-fluid">
|
|
<a href="{{ url_for('admin.dashboard') }}" class="navbar-brand text-decoration-none">🎛️ AccountForger - Admin Panel</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">
|
|
<!-- Navigation removed - access via sidebar -->
|
|
</ul>
|
|
<div class="d-flex align-items-center">
|
|
<div id="session-timer" class="timer-normal me-3">
|
|
⏱️ <span id="timer-display">5:00</span>
|
|
</div>
|
|
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
|
|
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-light btn-sm me-2">👤 Profil</a>
|
|
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-light btn-sm">Abmelden</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Sidebar Navigation -->
|
|
<aside class="sidebar" id="sidebar">
|
|
<ul class="sidebar-nav">
|
|
<li class="nav-item {% if request.endpoint in ['customers.customers', 'customers.customers_licenses', 'customers.edit_customer', 'customers.create_customer', 'licenses.edit_license', 'licenses.create_license', 'batch.batch_create'] %}has-active-child{% endif %}">
|
|
<a class="nav-link has-submenu {% if request.endpoint == 'customers.customers_licenses' %}active{% endif %}" href="{{ url_for('customers.customers_licenses') }}">
|
|
<i class="bi bi-people"></i>
|
|
<span>Kunden & Lizenzen</span>
|
|
</a>
|
|
<ul class="sidebar-submenu">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'customers.customers' %}active{% endif %}" href="{{ url_for('customers.customers') }}">
|
|
<i class="bi bi-list"></i>
|
|
<span>Alle Kunden</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'licenses.licenses' %}active{% endif %}" href="{{ url_for('licenses.licenses') }}">
|
|
<i class="bi bi-card-list"></i>
|
|
<span>Alle Lizenzen</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'customers.create_customer' %}active{% endif %}" href="{{ url_for('customers.create_customer') }}">
|
|
<i class="bi bi-person-plus"></i>
|
|
<span>Neuer Kunde</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'licenses.create_license' %}active{% endif %}" href="{{ url_for('licenses.create_license') }}">
|
|
<i class="bi bi-plus-circle"></i>
|
|
<span>Neue Lizenz</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'batch.batch_create' %}active{% endif %}" href="{{ url_for('batch.batch_create') }}">
|
|
<i class="bi bi-stack"></i>
|
|
<span>Batch-Erstellung</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint and request.endpoint.startswith('leads.') %}active{% endif %}" href="{{ url_for('leads.lead_management') }}">
|
|
<i class="bi bi-people"></i>
|
|
<span>Lead Management</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item {% if request.endpoint in ['resources.resources', 'resources.add_resources'] %}has-active-child{% endif %}">
|
|
<a class="nav-link has-submenu {% if request.endpoint == 'resources.resources' %}active{% endif %}" href="{{ url_for('resources.resources') }}">
|
|
<i class="bi bi-box-seam"></i>
|
|
<span>Ressourcen Pool</span>
|
|
</a>
|
|
<ul class="sidebar-submenu">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'resources.add_resources' %}active{% endif %}" href="{{ url_for('resources.add_resources') }}">
|
|
<i class="bi bi-plus-square"></i>
|
|
<span>Ressourcen hinzufügen</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li class="nav-item {% if request.endpoint in ['monitoring.unified_monitoring', 'monitoring.live_dashboard', 'monitoring.alerts'] %}has-active-child{% endif %}">
|
|
<a class="nav-link {% if request.endpoint == 'monitoring.unified_monitoring' %}active{% endif %}" href="{{ url_for('monitoring.unified_monitoring') }}">
|
|
<i class="bi bi-activity"></i>
|
|
<span>Monitoring</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item {% if request.endpoint in ['admin.audit_log', 'admin.backups', 'admin.blocked_ips', 'admin.license_config'] %}has-active-child{% endif %}">
|
|
<a class="nav-link has-submenu" href="{{ url_for('admin.license_config') }}">
|
|
<i class="bi bi-tools"></i>
|
|
<span>Administration</span>
|
|
</a>
|
|
<ul class="sidebar-submenu">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'admin.audit_log' %}active{% endif %}" href="{{ url_for('admin.audit_log') }}">
|
|
<i class="bi bi-journal-text"></i>
|
|
<span>Audit-Log</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'admin.backups' %}active{% endif %}" href="{{ url_for('admin.backups') }}">
|
|
<i class="bi bi-cloud-download"></i>
|
|
<span>Backups</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'admin.blocked_ips' %}active{% endif %}" href="{{ url_for('admin.blocked_ips') }}">
|
|
<i class="bi bi-slash-circle"></i>
|
|
<span>Gesperrte IPs</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</aside>
|
|
|
|
<!-- Main Content Area -->
|
|
<div class="main-content" id="main-content">
|
|
<!-- Page Content -->
|
|
<div class="container-fluid p-4">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Session Warning Modal -->
|
|
<div id="session-warning" class="session-warning-modal" style="display: none;">
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
<strong>⚠️ Session läuft ab!</strong><br>
|
|
Ihre Session läuft in weniger als 1 Minute ab.<br>
|
|
<button type="button" class="btn btn-sm btn-success mt-2" onclick="extendSession()">
|
|
Session verlängern
|
|
</button>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|
|
|
<script>
|
|
// Session-Timer Konfiguration
|
|
const SESSION_TIMEOUT = 5 * 60; // 5 Minuten in Sekunden
|
|
let timeRemaining = SESSION_TIMEOUT;
|
|
let timerInterval;
|
|
let warningShown = false;
|
|
let lastActivity = Date.now();
|
|
|
|
// Timer Display Update
|
|
function updateTimerDisplay() {
|
|
const minutes = Math.floor(timeRemaining / 60);
|
|
const seconds = timeRemaining % 60;
|
|
const display = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
document.getElementById('timer-display').textContent = display;
|
|
|
|
// Timer-Farbe ändern
|
|
const timerElement = document.getElementById('session-timer');
|
|
timerElement.className = timerElement.className.replace(/timer-\w+/, '');
|
|
|
|
if (timeRemaining <= 30) {
|
|
timerElement.classList.add('timer-critical');
|
|
} else if (timeRemaining <= 60) {
|
|
timerElement.classList.add('timer-danger');
|
|
if (!warningShown) {
|
|
showSessionWarning();
|
|
warningShown = true;
|
|
}
|
|
} else if (timeRemaining <= 120) {
|
|
timerElement.classList.add('timer-warning');
|
|
} else {
|
|
timerElement.classList.add('timer-normal');
|
|
warningShown = false;
|
|
hideSessionWarning();
|
|
}
|
|
}
|
|
|
|
// Session Warning anzeigen
|
|
function showSessionWarning() {
|
|
document.getElementById('session-warning').style.display = 'block';
|
|
}
|
|
|
|
// Session Warning verstecken
|
|
function hideSessionWarning() {
|
|
document.getElementById('session-warning').style.display = 'none';
|
|
}
|
|
|
|
// Timer zurücksetzen
|
|
function resetTimer() {
|
|
timeRemaining = SESSION_TIMEOUT;
|
|
lastActivity = Date.now();
|
|
updateTimerDisplay();
|
|
}
|
|
|
|
// Session verlängern
|
|
function extendSession() {
|
|
fetch('{{ url_for('auth.heartbeat') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'ok') {
|
|
resetTimer();
|
|
}
|
|
})
|
|
.catch(error => console.error('Heartbeat error:', error));
|
|
}
|
|
|
|
// Timer Countdown
|
|
function countdown() {
|
|
timeRemaining--;
|
|
updateTimerDisplay();
|
|
|
|
if (timeRemaining <= 0) {
|
|
clearInterval(timerInterval);
|
|
window.location.href = '{{ url_for('auth.logout') }}';
|
|
}
|
|
}
|
|
|
|
// Aktivitäts-Tracking
|
|
function trackActivity() {
|
|
const now = Date.now();
|
|
// Nur wenn mehr als 5 Sekunden seit letzter Aktivität
|
|
if (now - lastActivity > 5000) {
|
|
lastActivity = now;
|
|
extendSession(); // resetTimer() wird in extendSession nach erfolgreicher Response aufgerufen
|
|
}
|
|
}
|
|
|
|
// Event Listeners für Benutzeraktivität
|
|
document.addEventListener('click', trackActivity);
|
|
document.addEventListener('keypress', trackActivity);
|
|
document.addEventListener('mousemove', () => {
|
|
const now = Date.now();
|
|
// Mausbewegung nur alle 30 Sekunden tracken
|
|
if (now - lastActivity > 30000) {
|
|
trackActivity();
|
|
}
|
|
});
|
|
|
|
// AJAX Interceptor für automatische Session-Verlängerung
|
|
const originalFetch = window.fetch;
|
|
window.fetch = function(...args) {
|
|
// Nur für non-heartbeat requests den Timer verlängern
|
|
if (!args[0].includes('/heartbeat')) {
|
|
trackActivity();
|
|
}
|
|
return originalFetch.apply(this, args);
|
|
};
|
|
|
|
// Timer starten
|
|
timerInterval = setInterval(countdown, 1000);
|
|
updateTimerDisplay();
|
|
|
|
// Initial Heartbeat
|
|
extendSession();
|
|
|
|
// Preserve show_test parameter across navigation
|
|
function preserveShowTestParameter() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const showTest = urlParams.get('show_test');
|
|
|
|
if (showTest === 'true') {
|
|
// Update all internal links to include show_test parameter
|
|
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
// Skip if already has parameters or is just a fragment
|
|
if (!href.includes('?') && !href.startsWith('#')) {
|
|
link.setAttribute('href', href + '?show_test=true');
|
|
} else if (href.includes('?') && !href.includes('show_test=')) {
|
|
link.setAttribute('href', href + '&show_test=true');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Client-side table sorting
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Preserve show_test parameter on page load
|
|
preserveShowTestParameter();
|
|
|
|
// Initialize all sortable tables
|
|
const sortableTables = document.querySelectorAll('.sortable-table');
|
|
|
|
sortableTables.forEach(table => {
|
|
const headers = table.querySelectorAll('th.sortable');
|
|
|
|
headers.forEach((header, index) => {
|
|
header.addEventListener('click', function() {
|
|
sortTable(table, index, header);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
function sortTable(table, columnIndex, header) {
|
|
const tbody = table.querySelector('tbody');
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
const isNumeric = header.dataset.type === 'numeric';
|
|
const isDate = header.dataset.type === 'date';
|
|
|
|
// Determine sort direction
|
|
let direction = 'asc';
|
|
if (header.classList.contains('asc')) {
|
|
direction = 'desc';
|
|
}
|
|
|
|
// Remove all sort classes from headers
|
|
table.querySelectorAll('th.sortable').forEach(th => {
|
|
th.classList.remove('asc', 'desc');
|
|
});
|
|
|
|
// Add appropriate class to clicked header
|
|
header.classList.add(direction);
|
|
|
|
// Sort rows
|
|
rows.sort((a, b) => {
|
|
let aValue = a.cells[columnIndex].textContent.trim();
|
|
let bValue = b.cells[columnIndex].textContent.trim();
|
|
|
|
// Handle different data types
|
|
if (isNumeric) {
|
|
aValue = parseFloat(aValue.replace(/[^0-9.-]/g, '')) || 0;
|
|
bValue = parseFloat(bValue.replace(/[^0-9.-]/g, '')) || 0;
|
|
} else if (isDate) {
|
|
// Parse German date format (DD.MM.YYYY HH:MM)
|
|
aValue = parseGermanDate(aValue);
|
|
bValue = parseGermanDate(bValue);
|
|
} else {
|
|
// Text comparison with locale support for umlauts
|
|
aValue = aValue.toLowerCase();
|
|
bValue = bValue.toLowerCase();
|
|
}
|
|
|
|
if (aValue < bValue) return direction === 'asc' ? -1 : 1;
|
|
if (aValue > bValue) return direction === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
// Reorder rows in DOM
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
}
|
|
|
|
function parseGermanDate(dateStr) {
|
|
// Handle DD.MM.YYYY HH:MM format
|
|
const parts = dateStr.match(/(\d{2})\.(\d{2})\.(\d{4})\s*(\d{2}:\d{2})?/);
|
|
if (parts) {
|
|
const [_, day, month, year, time] = parts;
|
|
const timeStr = time || '00:00';
|
|
return new Date(`${year}-${month}-${day}T${timeStr}`);
|
|
}
|
|
return new Date(0);
|
|
}
|
|
</script>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html> |