392 Zeilen
18 KiB
HTML
392 Zeilen
18 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Lizenzübersicht{% endblock %}
|
||
|
||
{% macro sortable_header(label, field, current_sort, current_order) %}
|
||
<th>
|
||
{% if current_sort == field %}
|
||
<a href="{{ url_for('licenses', sort=field, order='desc' if current_order == 'asc' else 'asc', search=search, type=filter_type, status=filter_status, page=1) }}"
|
||
class="server-sortable">
|
||
{% else %}
|
||
<a href="{{ url_for('licenses', sort=field, order='asc', search=search, type=filter_type, status=filter_status, page=1) }}"
|
||
class="server-sortable">
|
||
{% endif %}
|
||
{{ label }}
|
||
<span class="sort-indicator{% if current_sort == field %} active{% endif %}">
|
||
{% if current_sort == field %}
|
||
{% if current_order == 'asc' %}↑{% else %}↓{% endif %}
|
||
{% else %}
|
||
↕
|
||
{% endif %}
|
||
</span>
|
||
</a>
|
||
</th>
|
||
{% endmacro %}
|
||
|
||
{% block extra_css %}
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container py-5">
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h2>Lizenzübersicht</h2>
|
||
<div>
|
||
<a href="/customers-licenses" class="btn btn-success">
|
||
<i class="bi bi-layout-split"></i> Kombinierte Ansicht
|
||
</a>
|
||
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
||
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
||
<div class="btn-group">
|
||
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||
📥 Export
|
||
</button>
|
||
<ul class="dropdown-menu">
|
||
<li><a class="dropdown-item" href="/export/licenses?format=excel">📊 Excel (.xlsx)</a></li>
|
||
<li><a class="dropdown-item" href="/export/licenses?format=csv">📄 CSV (.csv)</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Such- und Filterformular -->
|
||
<div class="card mb-3">
|
||
<div class="card-body">
|
||
<form method="get" action="/licenses" id="filterForm">
|
||
<div class="row g-3 align-items-end">
|
||
<div class="col-md-4">
|
||
<label for="search" class="form-label">🔍 Suchen</label>
|
||
<input type="text" class="form-control" id="search" name="search"
|
||
placeholder="Lizenzschlüssel, Kunde, E-Mail..."
|
||
value="{{ search }}">
|
||
</div>
|
||
<div class="col-md-2">
|
||
<label for="type" class="form-label">Typ</label>
|
||
<select class="form-select" id="type" name="type">
|
||
<option value="">Alle Typen</option>
|
||
<option value="full" {% if filter_type == 'full' %}selected{% endif %}>Vollversion</option>
|
||
<option value="test" {% if filter_type == 'test' %}selected{% endif %}>Testversion</option>
|
||
<option value="test_data" {% if filter_type == 'test_data' %}selected{% endif %}>🧪 Testdaten</option>
|
||
<option value="live_data" {% if filter_type == 'live_data' %}selected{% endif %}>🚀 Live-Daten</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label for="status" class="form-label">Status</label>
|
||
<select class="form-select" id="status" name="status">
|
||
<option value="">Alle Status</option>
|
||
<option value="active" {% if filter_status == 'active' %}selected{% endif %}>✅ Aktiv</option>
|
||
<option value="expiring" {% if filter_status == 'expiring' %}selected{% endif %}>⏰ Läuft bald ab</option>
|
||
<option value="expired" {% if filter_status == 'expired' %}selected{% endif %}>⚠️ Abgelaufen</option>
|
||
<option value="inactive" {% if filter_status == 'inactive' %}selected{% endif %}>❌ Deaktiviert</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<a href="/licenses" class="btn btn-outline-secondary">Zurücksetzen</a>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
{% if search or filter_type or filter_status %}
|
||
<div class="mt-2">
|
||
<small class="text-muted">
|
||
Gefiltert: {{ total }} Ergebnisse
|
||
{% if search %} | Suche: <strong>{{ search }}</strong>{% endif %}
|
||
{% if filter_type %} | Typ: <strong>{{ 'Vollversion' if filter_type == 'full' else 'Testversion' }}</strong>{% endif %}
|
||
{% if filter_status %} | Status: <strong>{{ filter_status }}</strong>{% endif %}
|
||
</small>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-body p-0">
|
||
<div class="table-container">
|
||
<table class="table table-hover table-sticky mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th class="checkbox-cell">
|
||
<input type="checkbox" class="form-check-input form-check-input-custom" id="selectAll">
|
||
</th>
|
||
{{ sortable_header('ID', 'id', sort, order) }}
|
||
{{ sortable_header('Lizenzschlüssel', 'license_key', sort, order) }}
|
||
{{ sortable_header('Kunde', 'customer', sort, order) }}
|
||
{{ sortable_header('E-Mail', 'email', sort, order) }}
|
||
{{ sortable_header('Typ', 'type', sort, order) }}
|
||
{{ sortable_header('Gültig von', 'valid_from', sort, order) }}
|
||
{{ sortable_header('Gültig bis', 'valid_until', sort, order) }}
|
||
{{ sortable_header('Status', 'status', sort, order) }}
|
||
{{ sortable_header('Aktiv', 'active', sort, order) }}
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for license in licenses %}
|
||
<tr>
|
||
<td class="checkbox-cell">
|
||
<input type="checkbox" class="form-check-input form-check-input-custom license-checkbox" value="{{ license[0] }}">
|
||
</td>
|
||
<td>{{ license[0] }}</td>
|
||
<td>
|
||
<div class="d-flex align-items-center">
|
||
<code class="me-2">{{ license[1] }}</code>
|
||
<button class="btn btn-sm btn-outline-secondary btn-copy" onclick="copyToClipboard('{{ license[1] }}', this)" title="Kopieren">
|
||
📋
|
||
</button>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
{{ license[2] }}
|
||
{% if license[8] %}
|
||
<span class="badge bg-secondary ms-1" title="Testdaten">🧪</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ license[3] or '-' }}</td>
|
||
<td>
|
||
{% if license[4] == 'full' %}
|
||
<span class="badge bg-success">Vollversion</span>
|
||
{% else %}
|
||
<span class="badge bg-warning">Testversion</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ license[5].strftime('%d.%m.%Y') }}</td>
|
||
<td>{{ license[6].strftime('%d.%m.%Y') }}</td>
|
||
<td>
|
||
{% if license[9] == 'abgelaufen' %}
|
||
<span class="status-abgelaufen">⚠️ Abgelaufen</span>
|
||
{% elif license[9] == 'läuft bald ab' %}
|
||
<span class="status-ablaufend">⏰ Läuft bald ab</span>
|
||
{% elif license[9] == 'deaktiviert' %}
|
||
<span class="status-deaktiviert">❌ Deaktiviert</span>
|
||
{% else %}
|
||
<span class="status-aktiv">✅ Aktiv</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<div class="form-check form-switch form-switch-custom">
|
||
<input class="form-check-input" type="checkbox"
|
||
id="active_{{ license[0] }}"
|
||
{{ 'checked' if license[7] else '' }}
|
||
onchange="toggleLicenseStatus({{ license[0] }}, this.checked)">
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="btn-group btn-group-sm" role="group">
|
||
<a href="/license/edit/{{ license[0] }}" class="btn btn-outline-primary">✏️ Bearbeiten</a>
|
||
<form method="post" action="/license/delete/{{ license[0] }}" style="display: inline;" onsubmit="return confirm('Wirklich löschen?');">
|
||
<button type="submit" class="btn btn-outline-danger">🗑️ Löschen</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
{% if not licenses %}
|
||
<div class="text-center py-5">
|
||
{% if search %}
|
||
<p class="text-muted">Keine Lizenzen gefunden für: <strong>{{ search }}</strong></p>
|
||
<a href="/licenses" class="btn btn-secondary">Alle Lizenzen anzeigen</a>
|
||
{% else %}
|
||
<p class="text-muted">Noch keine Lizenzen vorhanden.</p>
|
||
<a href="/create" class="btn btn-primary">Erste Lizenz erstellen</a>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
{% if total_pages > 1 %}
|
||
<nav aria-label="Seitennavigation" class="mt-3">
|
||
<ul class="pagination justify-content-center">
|
||
<!-- Erste Seite -->
|
||
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||
<a class="page-link" href="{{ url_for('licenses', page=1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">Erste</a>
|
||
</li>
|
||
|
||
<!-- Vorherige Seite -->
|
||
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||
<a class="page-link" href="{{ url_for('licenses', page=page-1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">←</a>
|
||
</li>
|
||
|
||
<!-- Seitenzahlen -->
|
||
{% for p in range(1, total_pages + 1) %}
|
||
{% if p >= page - 2 and p <= page + 2 %}
|
||
<li class="page-item {% if p == page %}active{% endif %}">
|
||
<a class="page-link" href="{{ url_for('licenses', page=p, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">{{ p }}</a>
|
||
</li>
|
||
{% endif %}
|
||
{% endfor %}
|
||
|
||
<!-- Nächste Seite -->
|
||
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||
<a class="page-link" href="{{ url_for('licenses', page=page+1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">→</a>
|
||
</li>
|
||
|
||
<!-- Letzte Seite -->
|
||
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||
<a class="page-link" href="{{ url_for('licenses', page=total_pages, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">Letzte</a>
|
||
</li>
|
||
</ul>
|
||
<p class="text-center text-muted">
|
||
Seite {{ page }} von {{ total_pages }} | Gesamt: {{ total }} Lizenzen
|
||
</p>
|
||
</nav>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bulk Actions Bar -->
|
||
<div class="bulk-actions" id="bulkActionsBar">
|
||
<div>
|
||
<span id="selectedCount">0</span> Lizenzen ausgewählt
|
||
</div>
|
||
<div>
|
||
<button class="btn btn-success btn-sm me-2" onclick="bulkActivate()">✅ Aktivieren</button>
|
||
<button class="btn btn-warning btn-sm me-2" onclick="bulkDeactivate()">⏸️ Deaktivieren</button>
|
||
<button class="btn btn-danger btn-sm" onclick="bulkDelete()">🗑️ Löschen</button>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
// Live Filtering
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const filterForm = document.getElementById('filterForm');
|
||
const searchInput = document.getElementById('search');
|
||
const typeSelect = document.getElementById('type');
|
||
const statusSelect = document.getElementById('status');
|
||
|
||
// Debounce timer für Suchfeld
|
||
let searchTimeout;
|
||
|
||
// Live-Filter für Suchfeld (mit 300ms Verzögerung)
|
||
searchInput.addEventListener('input', function() {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
filterForm.submit();
|
||
}, 300);
|
||
});
|
||
|
||
// Live-Filter für Dropdowns (sofort)
|
||
typeSelect.addEventListener('change', function() {
|
||
filterForm.submit();
|
||
});
|
||
|
||
statusSelect.addEventListener('change', function() {
|
||
filterForm.submit();
|
||
});
|
||
});
|
||
|
||
// Copy to Clipboard
|
||
function copyToClipboard(text, button) {
|
||
navigator.clipboard.writeText(text).then(function() {
|
||
button.classList.add('copied');
|
||
button.innerHTML = '✅';
|
||
setTimeout(function() {
|
||
button.classList.remove('copied');
|
||
button.innerHTML = '📋';
|
||
}, 2000);
|
||
});
|
||
}
|
||
|
||
// Toggle License Status
|
||
function toggleLicenseStatus(licenseId, isActive) {
|
||
fetch(`/api/license/${licenseId}/toggle`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ is_active: isActive })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Optional: Show success message
|
||
} else {
|
||
// Revert toggle on error
|
||
document.getElementById(`active_${licenseId}`).checked = !isActive;
|
||
alert('Fehler beim Ändern des Status');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Bulk Selection
|
||
const selectAll = document.getElementById('selectAll');
|
||
const checkboxes = document.querySelectorAll('.license-checkbox');
|
||
const bulkActionsBar = document.getElementById('bulkActionsBar');
|
||
const selectedCount = document.getElementById('selectedCount');
|
||
|
||
selectAll.addEventListener('change', function() {
|
||
checkboxes.forEach(cb => cb.checked = this.checked);
|
||
updateBulkActions();
|
||
});
|
||
|
||
checkboxes.forEach(cb => {
|
||
cb.addEventListener('change', updateBulkActions);
|
||
});
|
||
|
||
function updateBulkActions() {
|
||
const checkedBoxes = document.querySelectorAll('.license-checkbox:checked');
|
||
const count = checkedBoxes.length;
|
||
|
||
if (count > 0) {
|
||
bulkActionsBar.classList.add('show');
|
||
selectedCount.textContent = count;
|
||
} else {
|
||
bulkActionsBar.classList.remove('show');
|
||
}
|
||
|
||
// Update select all checkbox
|
||
selectAll.checked = count === checkboxes.length && count > 0;
|
||
selectAll.indeterminate = count > 0 && count < checkboxes.length;
|
||
}
|
||
|
||
// Bulk Actions
|
||
function getSelectedIds() {
|
||
return Array.from(document.querySelectorAll('.license-checkbox:checked'))
|
||
.map(cb => cb.value);
|
||
}
|
||
|
||
function bulkActivate() {
|
||
const ids = getSelectedIds();
|
||
if (confirm(`${ids.length} Lizenzen aktivieren?`)) {
|
||
performBulkAction('/api/licenses/bulk-activate', ids);
|
||
}
|
||
}
|
||
|
||
function bulkDeactivate() {
|
||
const ids = getSelectedIds();
|
||
if (confirm(`${ids.length} Lizenzen deaktivieren?`)) {
|
||
performBulkAction('/api/licenses/bulk-deactivate', ids);
|
||
}
|
||
}
|
||
|
||
function bulkDelete() {
|
||
const ids = getSelectedIds();
|
||
if (confirm(`${ids.length} Lizenzen wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden!`)) {
|
||
performBulkAction('/api/licenses/bulk-delete', ids);
|
||
}
|
||
}
|
||
|
||
function performBulkAction(url, ids) {
|
||
fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ ids: ids })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
location.reload();
|
||
} else {
|
||
alert('Fehler bei der Bulk-Aktion: ' + data.message);
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
{% endblock %} |