Test zu Fake geändert, weil Namensproblem
Dieser Commit ist enthalten in:
@@ -4,13 +4,20 @@
|
||||
|
||||
{% macro sortable_header(label, field, current_sort, current_order) %}
|
||||
<th>
|
||||
{% set base_url = url_for('licenses.licenses') %}
|
||||
{% set params = [] %}
|
||||
{% if search %}{% set _ = params.append('search=' + search|urlencode) %}{% endif %}
|
||||
{% for type in filter_types %}{% set _ = params.append('types[]=' + type|urlencode) %}{% endfor %}
|
||||
{% for status in filter_statuses %}{% set _ = params.append('statuses[]=' + status|urlencode) %}{% endfor %}
|
||||
{% set _ = params.append('sort=' + field) %}
|
||||
{% if current_sort == field %}
|
||||
<a href="{{ url_for('licenses.licenses', sort=field, order='desc' if current_order == 'asc' else 'asc', search=search, type=filter_type, status=filter_status, page=1) }}"
|
||||
class="server-sortable">
|
||||
{% set _ = params.append('order=' + ('desc' if current_order == 'asc' else 'asc')) %}
|
||||
{% else %}
|
||||
<a href="{{ url_for('licenses.licenses', sort=field, order='asc', search=search, type=filter_type, status=filter_status, page=1) }}"
|
||||
class="server-sortable">
|
||||
{% set _ = params.append('order=asc') %}
|
||||
{% endif %}
|
||||
{% set _ = params.append('page=1') %}
|
||||
|
||||
<a href="{{ base_url }}?{{ params|join('&') }}" class="server-sortable">
|
||||
{{ label }}
|
||||
<span class="sort-indicator{% if current_sort == field %} active{% endif %}">
|
||||
{% if current_sort == field %}
|
||||
@@ -24,6 +31,46 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.filter-group {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.filter-group h6 {
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge.bg-info {
|
||||
background-color: #0dcaf0 !important;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.active-filters .badge {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.35em 0.65em;
|
||||
}
|
||||
|
||||
.active-filters a {
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.active-filters a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#advancedFilters {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -36,46 +83,162 @@
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form method="get" action="{{ url_for('licenses.licenses') }}" id="filterForm">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<!-- Suchfeld -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-8">
|
||||
<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="{{ url_for('licenses.licenses') }}" class="btn btn-outline-secondary">Zurücksetzen</a>
|
||||
<div class="col-md-4 text-end">
|
||||
<label class="form-label d-block"> </label>
|
||||
<button type="button" class="btn btn-outline-primary me-2" onclick="toggleFilters()">
|
||||
<i class="bi bi-funnel"></i> Filter
|
||||
</button>
|
||||
<a href="{{ url_for('licenses.licenses') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-clockwise"></i> Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erweiterter Filterbereich -->
|
||||
<div id="advancedFilters" class="collapse {{ 'show' if filter_types or filter_status else '' }}">
|
||||
<hr class="my-3">
|
||||
|
||||
<!-- Lizenztyp Filter (OR Logic) -->
|
||||
<div class="filter-group mb-3">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<h6 class="mb-0 me-2">Lizenztyp</h6>
|
||||
<span class="badge bg-info">ODER</span>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="types[]" value="full" id="typeFullversion"
|
||||
{% if 'full' in (filter_types or []) %}checked{% endif %}>
|
||||
<label class="form-check-label" for="typeFullversion">
|
||||
<span class="badge bg-success">Vollversion</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="types[]" value="test" id="typeTestversion"
|
||||
{% if 'test' in (filter_types or []) %}checked{% endif %}>
|
||||
<label class="form-check-label" for="typeTestversion">
|
||||
<span class="badge bg-warning">Testversion</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter (OR Logic) -->
|
||||
<div class="filter-group mb-3">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<h6 class="mb-0 me-2">Status</h6>
|
||||
<span class="badge bg-info">ODER</span>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="statuses[]" value="active" id="statusActive"
|
||||
{% if 'active' in (filter_statuses or []) %}checked{% endif %}>
|
||||
<label class="form-check-label" for="statusActive">
|
||||
✅ Aktiv
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="statuses[]" value="expiring" id="statusExpiring"
|
||||
{% if 'expiring' in (filter_statuses or []) %}checked{% endif %}>
|
||||
<label class="form-check-label" for="statusExpiring">
|
||||
⏰ Läuft bald ab
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="statuses[]" value="expired" id="statusExpired"
|
||||
{% if 'expired' in (filter_statuses or []) %}checked{% endif %}>
|
||||
<label class="form-check-label" for="statusExpired">
|
||||
⚠️ Abgelaufen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="statuses[]" value="inactive" id="statusInactive"
|
||||
{% if 'inactive' in (filter_statuses or []) %}checked{% endif %}>
|
||||
<label class="form-check-label" for="statusInactive">
|
||||
❌ Deaktiviert
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Filters / Presets -->
|
||||
<div class="filter-group">
|
||||
<h6 class="mb-2">Schnellfilter</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="applyPreset('expiring_soon')">
|
||||
<i class="bi bi-clock-history"></i> Läuft in 7 Tagen ab
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="applyPreset('all_test')">
|
||||
<i class="bi bi-bug"></i> Alle Testversionen
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="applyPreset('active_full')">
|
||||
<i class="bi bi-shield-check"></i> Aktive Vollversionen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden fields for sorting -->
|
||||
<input type="hidden" name="sort" value="{{ sort }}">
|
||||
<input type="hidden" name="order" value="{{ order }}">
|
||||
</form>
|
||||
{% if search or filter_type or filter_status %}
|
||||
{% if search or filter_types or filter_statuses %}
|
||||
<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 class="d-flex align-items-center justify-content-between">
|
||||
<small class="text-muted">
|
||||
Gefiltert: {{ total }} Ergebnisse
|
||||
</small>
|
||||
<div class="active-filters">
|
||||
{% if search %}
|
||||
<span class="badge bg-secondary me-1">
|
||||
<i class="bi bi-search"></i> {{ search }}
|
||||
{% set clear_search_params = [] %}
|
||||
{% for type in filter_types %}{% set _ = clear_search_params.append('types[]=' + type|urlencode) %}{% endfor %}
|
||||
{% for status in filter_statuses %}{% set _ = clear_search_params.append('statuses[]=' + status|urlencode) %}{% endfor %}
|
||||
{% set _ = clear_search_params.append('sort=' + sort) %}
|
||||
{% set _ = clear_search_params.append('order=' + order) %}
|
||||
<a href="{{ url_for('licenses.licenses') }}?{{ clear_search_params|join('&') }}" class="text-white ms-1">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% for type in (filter_types or []) %}
|
||||
<span class="badge bg-primary me-1">
|
||||
{{ 'Vollversion' if type == 'full' else 'Testversion' }}
|
||||
<a href="#" onclick="removeFilter('type', '{{ type }}')" class="text-white ms-1">×</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% for status in (filter_statuses or []) %}
|
||||
<span class="badge bg-info me-1">
|
||||
{% if status == 'active' %}✅ Aktiv{% elif status == 'expiring' %}⏰ Läuft bald ab{% elif status == 'expired' %}⚠️ Abgelaufen{% else %}❌ Deaktiviert{% endif %}
|
||||
<a href="#" onclick="removeFilter('status', '{{ status }}')" class="text-white ms-1">×</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -119,8 +282,8 @@
|
||||
</td>
|
||||
<td>
|
||||
{{ license.customer_name }}
|
||||
{% if license.is_test %}
|
||||
<span class="badge bg-secondary ms-1" title="Testdaten">🧪</span>
|
||||
{% if license.is_fake %}
|
||||
<span class="badge bg-secondary ms-1" title="Fake-Daten">🧪</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>-</td>
|
||||
@@ -182,33 +345,41 @@
|
||||
{% if total_pages > 1 %}
|
||||
<nav aria-label="Seitennavigation" class="mt-3">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% set base_url = url_for('licenses.licenses') %}
|
||||
{% set base_params = [] %}
|
||||
{% if search %}{% set _ = base_params.append('search=' + search|urlencode) %}{% endif %}
|
||||
{% for type in filter_types %}{% set _ = base_params.append('types[]=' + type|urlencode) %}{% endfor %}
|
||||
{% for status in filter_statuses %}{% set _ = base_params.append('statuses[]=' + status|urlencode) %}{% endfor %}
|
||||
{% set _ = base_params.append('sort=' + sort) %}
|
||||
{% set _ = base_params.append('order=' + order) %}
|
||||
|
||||
<!-- Erste Seite -->
|
||||
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('licenses.licenses', page=1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">Erste</a>
|
||||
<a class="page-link" href="{{ base_url }}?{{ base_params|join('&') }}&page=1">Erste</a>
|
||||
</li>
|
||||
|
||||
<!-- Vorherige Seite -->
|
||||
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('licenses.licenses', page=page-1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">←</a>
|
||||
<a class="page-link" href="{{ base_url }}?{{ base_params|join('&') }}&page={{ page-1 }}">←</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.licenses', page=p, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">{{ p }}</a>
|
||||
<a class="page-link" href="{{ base_url }}?{{ base_params|join('&') }}&page={{ p }}">{{ 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.licenses', page=page+1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">→</a>
|
||||
<a class="page-link" href="{{ base_url }}?{{ base_params|join('&') }}&page={{ page+1 }}">→</a>
|
||||
</li>
|
||||
|
||||
<!-- Letzte Seite -->
|
||||
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('licenses.licenses', page=total_pages, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">Letzte</a>
|
||||
<a class="page-link" href="{{ base_url }}?{{ base_params|join('&') }}&page={{ total_pages }}">Letzte</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-center text-muted">
|
||||
@@ -235,12 +406,58 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Toggle Filter Panel
|
||||
function toggleFilters() {
|
||||
const filtersDiv = document.getElementById('advancedFilters');
|
||||
const bsCollapse = new bootstrap.Collapse(filtersDiv, {
|
||||
toggle: true
|
||||
});
|
||||
}
|
||||
|
||||
// Apply Preset Filters
|
||||
function applyPreset(preset) {
|
||||
const form = document.getElementById('filterForm');
|
||||
|
||||
// Clear existing filters
|
||||
document.querySelectorAll('input[name="types[]"]').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('input[name="statuses[]"]').forEach(cb => cb.checked = false);
|
||||
|
||||
switch(preset) {
|
||||
case 'expiring_soon':
|
||||
document.getElementById('statusExpiring').checked = true;
|
||||
break;
|
||||
case 'all_test':
|
||||
document.getElementById('typeTestversion').checked = true;
|
||||
break;
|
||||
case 'active_full':
|
||||
document.getElementById('typeFullversion').checked = true;
|
||||
document.getElementById('statusActive').checked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// Remove Individual Filter
|
||||
function removeFilter(filterType, value) {
|
||||
const form = document.getElementById('filterForm');
|
||||
|
||||
if (filterType === 'type') {
|
||||
const checkbox = document.querySelector(`input[name="types[]"][value="${value}"]`);
|
||||
if (checkbox) checkbox.checked = false;
|
||||
} else if (filterType === 'status') {
|
||||
const checkbox = document.querySelector(`input[name="statuses[]"][value="${value}"]`);
|
||||
if (checkbox) checkbox.checked = false;
|
||||
}
|
||||
|
||||
form.submit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -253,13 +470,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Live-Filter für Dropdowns (sofort)
|
||||
typeSelect.addEventListener('change', function() {
|
||||
filterForm.submit();
|
||||
});
|
||||
|
||||
statusSelect.addEventListener('change', function() {
|
||||
filterForm.submit();
|
||||
// Live-Filter für Checkboxen
|
||||
document.querySelectorAll('input[name="types[]"], input[name="statuses[]"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
filterForm.submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren