Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,455 @@
{% extends "base.html" %}
{% block title %}Lizenzübersicht{% endblock %}
{% 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 %}
{% if request.args.get('data_source') %}{% set _ = params.append('data_source=' + request.args.get('data_source')|urlencode) %}{% endif %}
{% 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) %}
{% if current_sort == field %}
{% set _ = params.append('order=' + ('desc' if current_order == 'asc' else 'asc')) %}
{% else %}
{% 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 %}
{% if current_order == 'asc' %}↑{% else %}↓{% endif %}
{% else %}
{% endif %}
</span>
</a>
</th>
{% 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 %}
<div class="container py-5">
<div class="mb-4">
<h2>Lizenzübersicht</h2>
</div>
<!-- Such- und Filterformular -->
<div class="card mb-3">
<div class="card-body">
<form method="get" action="{{ url_for('licenses.licenses') }}" id="filterForm">
<!-- Filter Controls -->
<div class="row g-3 mb-3">
<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="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>
<input type="text" class="form-control" id="search" name="search"
placeholder="Lizenzschlüssel, Kunde, E-Mail..."
value="{{ search }}">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search"></i> Suchen
</button>
</div>
</div>
<!-- Hidden fields for sorting and pagination -->
<input type="hidden" name="sort" value="{{ sort }}">
<input type="hidden" name="order" value="{{ order }}">
<!-- Note: Other filters are preserved by the form elements themselves -->
</form>
{% 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="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 %}
{% if show_fake %}{% set _ = clear_search_params.append('show_fake=1') %}{% endif %}
{% 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">&times;</a>
</span>
{% endif %}
</div>
</div>
</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.id }}">
</td>
<td>{{ license.id }}</td>
<td>
<div class="d-flex align-items-center">
<code class="me-2">{{ license.license_key }}</code>
<button class="btn btn-sm btn-outline-secondary btn-copy" onclick="copyToClipboard('{{ license.license_key }}', this)" title="Kopieren">
📋
</button>
</div>
</td>
<td>
{{ license.customer_name }}
{% if license.is_fake %}
<span class="badge bg-secondary ms-1" title="Fake-Daten">🧪</span>
{% endif %}
</td>
<td>-</td>
<td>
{% if license.license_type == 'full' %}
<span class="badge bg-success">Vollversion</span>
{% else %}
<span class="badge bg-warning">Testversion</span>
{% endif %}
</td>
<td>{{ license.valid_from.strftime('%d.%m.%Y') }}</td>
<td>{{ license.valid_until.strftime('%d.%m.%Y') }}</td>
<td>
{% if not license.is_active %}
<span class="status-deaktiviert">❌ Deaktiviert</span>
{% elif license.valid_until < now().date() %}
<span class="status-abgelaufen">⚠️ Abgelaufen</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.id }}"
{{ 'checked' if license.is_active else '' }}
onchange="toggleLicenseStatus({{ license.id }}, this.checked)">
</div>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{{ url_for('licenses.edit_license', license_id=license.id) }}" class="btn btn-outline-primary">✏️ Bearbeiten</a>
<form method="post" action="{{ url_for('licenses.delete_license', license_id=license.id) }}" 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="{{ url_for('licenses.licenses') }}" class="btn btn-secondary">Alle Lizenzen anzeigen</a>
{% else %}
<p class="text-muted">Noch keine Lizenzen vorhanden.</p>
<a href="{{ url_for('licenses.create_license') }}" 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">
{% 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="{{ 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="{{ 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="{{ 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="{{ 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="{{ base_url }}?{{ base_params|join('&') }}&page={{ total_pages }}">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');
});
// 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) {
// Build URL manually to avoid template rendering issues
const baseUrl = '{{ url_for("api.toggle_license", license_id=999999) }}'.replace('999999', licenseId);
fetch(baseUrl, {
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('{{ url_for('api.bulk_activate_licenses') }}', ids);
}
}
function bulkDeactivate() {
const ids = getSelectedIds();
if (confirm(`${ids.length} Lizenzen deaktivieren?`)) {
performBulkAction('{{ url_for('api.bulk_deactivate_licenses') }}', ids);
}
}
function bulkDelete() {
const ids = getSelectedIds();
if (confirm(`${ids.length} Lizenzen wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden!`)) {
performBulkAction('{{ url_for('api.bulk_delete_licenses') }}', 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) {
// Show warnings if any licenses were skipped
if (data.warnings && data.warnings.length > 0) {
let warningMessage = data.message + '\n\n';
warningMessage += 'Warnungen:\n';
data.warnings.forEach(warning => {
warningMessage += '⚠️ ' + warning + '\n';
});
alert(warningMessage);
}
location.reload();
} else {
alert('Fehler bei der Bulk-Aktion: ' + (data.error || data.message));
}
});
}
</script>
{% endblock %}