Lizenzübersicht besser
Dieser Commit ist enthalten in:
@@ -24,47 +24,48 @@ def licenses():
|
|||||||
|
|
||||||
# Get filter parameters
|
# Get filter parameters
|
||||||
search = request.args.get('search', '').strip()
|
search = request.args.get('search', '').strip()
|
||||||
filter_types = request.args.getlist('types[]') # Multi-select for types
|
data_source = request.args.get('data_source', 'real') # real, fake, all
|
||||||
filter_statuses = request.args.getlist('statuses[]') # Multi-select for statuses
|
license_type = request.args.get('license_type', '') # '', full, test
|
||||||
|
license_status = request.args.get('license_status', '') # '', active, expiring, expired, inactive
|
||||||
sort = request.args.get('sort', 'created_at')
|
sort = request.args.get('sort', 'created_at')
|
||||||
order = request.args.get('order', 'desc')
|
order = request.args.get('order', 'desc')
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
per_page = 50
|
per_page = 50
|
||||||
|
|
||||||
# Get all licenses (both fake and real data)
|
# Get licenses based on data source
|
||||||
|
if data_source == 'fake':
|
||||||
licenses_list = get_licenses(show_fake=True)
|
licenses_list = get_licenses(show_fake=True)
|
||||||
|
licenses_list = [l for l in licenses_list if l.get('is_fake')]
|
||||||
|
elif data_source == 'all':
|
||||||
|
licenses_list = get_licenses(show_fake=True)
|
||||||
|
else: # real
|
||||||
|
licenses_list = get_licenses(show_fake=False)
|
||||||
|
|
||||||
# Type filtering with OR logic
|
# Type filtering
|
||||||
if filter_types:
|
if license_type:
|
||||||
filtered_by_type = []
|
if license_type == 'full':
|
||||||
for license in licenses_list:
|
licenses_list = [l for l in licenses_list if l.get('license_type') == 'full']
|
||||||
# Check if license matches any selected type
|
elif license_type == 'test':
|
||||||
if 'full' in filter_types and license.get('license_type') == 'full' and not license.get('is_fake'):
|
licenses_list = [l for l in licenses_list if l.get('license_type') == 'test']
|
||||||
filtered_by_type.append(license)
|
|
||||||
elif 'test' in filter_types and license.get('license_type') == 'test' and not license.get('is_fake'):
|
# Status filtering
|
||||||
filtered_by_type.append(license)
|
if license_status:
|
||||||
licenses_list = filtered_by_type
|
now = datetime.now().date()
|
||||||
else:
|
filtered_licenses = []
|
||||||
# If no types selected, show only real data by default
|
|
||||||
licenses_list = [l for l in licenses_list if not l.get('is_fake')]
|
|
||||||
|
|
||||||
# Status filtering with OR logic
|
|
||||||
if filter_statuses:
|
|
||||||
now = datetime.now()
|
|
||||||
filtered_by_status = []
|
|
||||||
for license in licenses_list:
|
for license in licenses_list:
|
||||||
# Check if license matches any selected status
|
if license_status == 'active' and license.get('is_active') and license.get('valid_until') and license.get('valid_until') > now:
|
||||||
if 'active' in filter_statuses and license.get('is_active') and license.get('valid_until') and license.get('valid_until') > now:
|
filtered_licenses.append(license)
|
||||||
filtered_by_status.append(license)
|
elif license_status == 'expiring':
|
||||||
elif 'expiring' in filter_statuses:
|
|
||||||
expiry_threshold = now + timedelta(days=30)
|
expiry_threshold = now + timedelta(days=30)
|
||||||
if license.get('valid_until') and now < license.get('valid_until') <= expiry_threshold:
|
if license.get('valid_until') and now < license.get('valid_until') <= expiry_threshold:
|
||||||
filtered_by_status.append(license)
|
filtered_licenses.append(license)
|
||||||
elif 'expired' in filter_statuses and license.get('valid_until') and license.get('valid_until') <= now:
|
elif license_status == 'expired' and license.get('valid_until') and license.get('valid_until') <= now:
|
||||||
filtered_by_status.append(license)
|
filtered_licenses.append(license)
|
||||||
elif 'inactive' in filter_statuses and not license.get('is_active'):
|
elif license_status == 'inactive' and not license.get('is_active'):
|
||||||
filtered_by_status.append(license)
|
filtered_licenses.append(license)
|
||||||
licenses_list = filtered_by_status
|
|
||||||
|
licenses_list = filtered_licenses
|
||||||
|
|
||||||
# Search filtering
|
# Search filtering
|
||||||
if search:
|
if search:
|
||||||
@@ -84,8 +85,9 @@ def licenses():
|
|||||||
return render_template("licenses.html",
|
return render_template("licenses.html",
|
||||||
licenses=licenses_list,
|
licenses=licenses_list,
|
||||||
search=search,
|
search=search,
|
||||||
filter_types=filter_types,
|
data_source=data_source,
|
||||||
filter_statuses=filter_statuses,
|
license_type=license_type,
|
||||||
|
license_status=license_status,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
order=order,
|
order=order,
|
||||||
page=page,
|
page=page,
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
{% set base_url = url_for('licenses.licenses') %}
|
{% set base_url = url_for('licenses.licenses') %}
|
||||||
{% set params = [] %}
|
{% set params = [] %}
|
||||||
{% if search %}{% set _ = params.append('search=' + search|urlencode) %}{% endif %}
|
{% if search %}{% set _ = params.append('search=' + search|urlencode) %}{% endif %}
|
||||||
{% for type in filter_types %}{% set _ = params.append('types[]=' + type|urlencode) %}{% endfor %}
|
{% if request.args.get('data_source') %}{% set _ = params.append('data_source=' + request.args.get('data_source')|urlencode) %}{% endif %}
|
||||||
{% for status in filter_statuses %}{% set _ = params.append('statuses[]=' + status|urlencode) %}{% endfor %}
|
{% if request.args.get('license_type') %}{% set _ = params.append('license_type=' + request.args.get('license_type')|urlencode) %}{% endif %}
|
||||||
|
{% if request.args.get('license_status') %}{% set _ = params.append('license_status=' + request.args.get('license_status')|urlencode) %}{% endif %}
|
||||||
{% set _ = params.append('sort=' + field) %}
|
{% set _ = params.append('sort=' + field) %}
|
||||||
{% if current_sort == field %}
|
{% if current_sort == field %}
|
||||||
{% set _ = params.append('order=' + ('desc' if current_order == 'asc' else 'asc')) %}
|
{% set _ = params.append('order=' + ('desc' if current_order == 'asc' else 'asc')) %}
|
||||||
@@ -83,131 +84,62 @@
|
|||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" action="{{ url_for('licenses.licenses') }}" id="filterForm">
|
<form method="get" action="{{ url_for('licenses.licenses') }}" id="filterForm">
|
||||||
<!-- Suchfeld -->
|
<!-- Filter Controls -->
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-3">
|
||||||
|
<label for="dataSource" class="form-label">Datenquelle</label>
|
||||||
|
<select class="form-select" name="data_source" id="dataSource" onchange="this.form.submit()">
|
||||||
|
<option value="real" {% if request.args.get('data_source', 'real') == 'real' %}selected{% endif %}>Echte Lizenzen</option>
|
||||||
|
<option value="fake" {% if request.args.get('data_source') == 'fake' %}selected{% endif %}>🧪 Fake-Daten</option>
|
||||||
|
<option value="all" {% if request.args.get('data_source') == 'all' %}selected{% endif %}>Alle Daten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="licenseType" class="form-label">Lizenztyp</label>
|
||||||
|
<select class="form-select" name="license_type" id="licenseType" onchange="this.form.submit()">
|
||||||
|
<option value="" {% if not request.args.get('license_type') %}selected{% endif %}>Alle Typen</option>
|
||||||
|
<option value="full" {% if request.args.get('license_type') == 'full' %}selected{% endif %}>Vollversion</option>
|
||||||
|
<option value="test" {% if request.args.get('license_type') == 'test' %}selected{% endif %}>Testversion</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="licenseStatus" class="form-label">Status</label>
|
||||||
|
<select class="form-select" name="license_status" id="licenseStatus" onchange="this.form.submit()">
|
||||||
|
<option value="" {% if not request.args.get('license_status') %}selected{% endif %}>Alle Status</option>
|
||||||
|
<option value="active" {% if request.args.get('license_status') == 'active' %}selected{% endif %}>✅ Aktiv</option>
|
||||||
|
<option value="expiring" {% if request.args.get('license_status') == 'expiring' %}selected{% endif %}>⏰ Läuft bald ab</option>
|
||||||
|
<option value="expired" {% if request.args.get('license_status') == 'expired' %}selected{% endif %}>⚠️ Abgelaufen</option>
|
||||||
|
<option value="inactive" {% if request.args.get('license_status') == 'inactive' %}selected{% endif %}>❌ Deaktiviert</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
|
<a href="{{ url_for('licenses.licenses') }}" class="btn btn-secondary w-100">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Filter zurücksetzen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Field -->
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-9">
|
||||||
<label for="search" class="form-label">🔍 Suchen</label>
|
<label for="search" class="form-label">🔍 Suchen</label>
|
||||||
<input type="text" class="form-control" id="search" name="search"
|
<input type="text" class="form-control" id="search" name="search"
|
||||||
placeholder="Lizenzschlüssel, Kunde, E-Mail..."
|
placeholder="Lizenzschlüssel, Kunde, E-Mail..."
|
||||||
value="{{ search }}">
|
value="{{ search }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
<label class="form-label d-block"> </label>
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<button type="button" class="btn btn-outline-primary me-2" onclick="toggleFilters()">
|
<i class="bi bi-search"></i> Suchen
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden fields for sorting -->
|
<!-- Hidden fields for sorting and pagination -->
|
||||||
<input type="hidden" name="sort" value="{{ sort }}">
|
<input type="hidden" name="sort" value="{{ sort }}">
|
||||||
<input type="hidden" name="order" value="{{ order }}">
|
<input type="hidden" name="order" value="{{ order }}">
|
||||||
|
<!-- Note: Other filters are preserved by the form elements themselves -->
|
||||||
</form>
|
</form>
|
||||||
{% if search or filter_types or filter_statuses %}
|
{% if search or request.args.get('data_source') != 'real' or request.args.get('license_type') or request.args.get('license_status') %}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
@@ -220,23 +152,12 @@
|
|||||||
{% set clear_search_params = [] %}
|
{% set clear_search_params = [] %}
|
||||||
{% for type in filter_types %}{% set _ = clear_search_params.append('types[]=' + type|urlencode) %}{% endfor %}
|
{% 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 %}
|
{% for status in filter_statuses %}{% set _ = clear_search_params.append('statuses[]=' + status|urlencode) %}{% endfor %}
|
||||||
|
{% if show_fake %}{% set _ = clear_search_params.append('show_fake=1') %}{% endif %}
|
||||||
{% set _ = clear_search_params.append('sort=' + sort) %}
|
{% set _ = clear_search_params.append('sort=' + sort) %}
|
||||||
{% set _ = clear_search_params.append('order=' + order) %}
|
{% set _ = clear_search_params.append('order=' + order) %}
|
||||||
<a href="{{ url_for('licenses.licenses') }}?{{ clear_search_params|join('&') }}" class="text-white ms-1">×</a>
|
<a href="{{ url_for('licenses.licenses') }}?{{ clear_search_params|join('&') }}" class="text-white ms-1">×</a>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -406,76 +327,12 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<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
|
// Live Filtering
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const filterForm = document.getElementById('filterForm');
|
const filterForm = document.getElementById('filterForm');
|
||||||
const searchInput = document.getElementById('search');
|
const searchInput = document.getElementById('search');
|
||||||
|
|
||||||
// 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 Checkboxen
|
|
||||||
document.querySelectorAll('input[name="types[]"], input[name="statuses[]"]').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', function() {
|
|
||||||
filterForm.submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy to Clipboard
|
// Copy to Clipboard
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren