Kunden & Lizenzen - Part1

Dieser Commit ist enthalten in:
2025-06-09 19:13:44 +02:00
Ursprung dbd50bdde6
Commit 97b87465e4
12 geänderte Dateien mit 1625 neuen und 227 gelöschten Zeilen

Datei anzeigen

@@ -5,6 +5,7 @@
<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 %}
@@ -379,8 +380,30 @@
// 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');
@@ -455,6 +478,9 @@
}
</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>

Datei anzeigen

@@ -28,6 +28,9 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Kundenverwaltung</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>

Datei anzeigen

@@ -0,0 +1,443 @@
{% extends "base.html" %}
{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>👥 Kunden & Lizenzen</h2>
<div>
<a href="/create" class="btn btn-success"> 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">
📥 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>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
</div>
</div>
<div class="row">
<!-- Kundenliste (Links) -->
<div class="col-md-4 col-lg-3">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-people"></i> Kunden
<span class="badge bg-secondary float-end">{{ customers|length if customers else 0 }}</span>
</h5>
</div>
<div class="card-body p-0">
<!-- Suchfeld -->
<div class="p-3 border-bottom">
<input type="text" class="form-control mb-2" id="customerSearch"
placeholder="Kunde suchen..." autocomplete="off">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showTestCustomers"
{% if request.args.get('show_test', 'false').lower() == 'true' %}checked{% endif %}
onchange="toggleTestCustomers()">
<label class="form-check-label" for="showTestCustomers">
<small class="text-muted">Testkunden anzeigen</small>
</label>
</div>
</div>
<!-- Kundenliste -->
<div class="customer-list" style="max-height: 600px; overflow-y: auto;">
{% if customers %}
{% for customer in customers %}
<div class="customer-item p-3 border-bottom {% if customer[0] == selected_customer_id %}active{% endif %}"
data-customer-id="{{ customer[0] }}"
data-customer-name="{{ customer[1]|lower }}"
data-customer-email="{{ customer[2]|lower }}"
onclick="loadCustomerLicenses({{ customer[0] }})"
style="cursor: pointer;">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">{{ customer[1] }}</h6>
<small class="text-muted">{{ customer[2] }}</small>
</div>
<div class="text-end">
<span class="badge bg-primary">{{ customer[4] }}</span>
{% if customer[5] > 0 %}
<span class="badge bg-success">{{ customer[5] }}</span>
{% endif %}
{% if customer[6] > 0 %}
<span class="badge bg-danger">{{ customer[6] }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="p-4 text-center text-muted">
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3 mb-2">Keine Kunden vorhanden</p>
<small class="d-block mb-3">Erstellen Sie eine neue Lizenz, um automatisch einen Kunden anzulegen.</small>
<a href="/create" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle"></i> Neue Lizenz erstellen
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Lizenzdetails (Rechts) -->
<div class="col-md-8 col-lg-9">
<div class="card">
<div class="card-header bg-light">
{% if selected_customer %}
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">{{ selected_customer[1] }}</h5>
<small class="text-muted">{{ selected_customer[2] }}</small>
</div>
<div>
<a href="/customer/edit/{{ selected_customer[0] }}?ref=customers-licenses{% if request.args.get('show_test') %}&show_test=true{% endif %}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
<i class="bi bi-plus"></i> Neue Lizenz
</button>
</div>
</div>
{% else %}
<h5 class="mb-0">Wählen Sie einen Kunden aus</h5>
{% endif %}
</div>
<div class="card-body">
<div id="licenseContainer">
{% if selected_customer %}
{% if licenses %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Typ</th>
<th>Gültig von</th>
<th>Gültig bis</th>
<th>Status</th>
<th>Ressourcen</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for license in licenses %}
<tr>
<td>
<code>{{ license[1] }}</code>
<button class="btn btn-sm btn-link" onclick="copyToClipboard('{{ license[1] }}')">
<i class="bi bi-clipboard"></i>
</button>
</td>
<td>
<span class="badge {% if license[2] == 'full' %}bg-primary{% else %}bg-secondary{% endif %}">
{{ license[2]|upper }}
</span>
</td>
<td>{{ license[3].strftime('%d.%m.%Y') if license[3] else '-' }}</td>
<td>{{ license[4].strftime('%d.%m.%Y') if license[4] else '-' }}</td>
<td>
<span class="badge
{% if license[6] == 'aktiv' %}bg-success
{% elif license[6] == 'läuft bald ab' %}bg-warning
{% elif license[6] == 'abgelaufen' %}bg-danger
{% else %}bg-secondary{% endif %}">
{{ license[6] }}
</span>
</td>
<td>
{% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %}
{% if license[8] > 0 %}📡 {{ license[8] }}{% endif %}
{% if license[9] > 0 %}📱 {{ license[9] }}{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus({{ license[0] }}, {{ license[5] }})">
<i class="bi bi-power"></i>
</button>
<a href="/license/edit/{{ license[0] }}{% if request.args.get('show_test') %}?ref=customers-licenses&show_test=true{% endif %}" class="btn btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
<button class="btn btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
<i class="bi bi-plus"></i> Erste Lizenz erstellen
</button>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-arrow-left text-muted" style="font-size: 3rem;"></i>
<p class="text-muted mt-3">Wählen Sie einen Kunden aus der Liste aus</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal für neue Lizenz -->
<style>
.customer-item {
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.customer-item:hover {
background-color: #f8f9fa;
border-left-color: #dee2e6;
}
.customer-item.active {
background-color: #e7f3ff;
border-left-color: #0d6efd;
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
// Globale Variablen
let currentCustomerId = {{ selected_customer_id or 'null' }};
// Kundensuche
document.getElementById('customerSearch').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const customerItems = document.querySelectorAll('.customer-item');
customerItems.forEach(item => {
const name = item.dataset.customerName;
const email = item.dataset.customerEmail;
if (name.includes(searchTerm) || email.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
// Lade Lizenzen eines Kunden
function loadCustomerLicenses(customerId) {
// Aktiven Status aktualisieren
document.querySelectorAll('.customer-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-customer-id="${customerId}"]`).classList.add('active');
// URL aktualisieren ohne Reload (behalte show_test Parameter)
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('customer_id', customerId);
window.history.pushState({}, '', currentUrl.toString());
// Lade Lizenzen via AJAX
const container = document.getElementById('licenseContainer');
const cardHeader = document.querySelector('.card-header.bg-light');
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
fetch(`/api/customer/${customerId}/licenses`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Update header with customer info
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
const customerName = customerItem.querySelector('h6').textContent;
const customerEmail = customerItem.querySelector('small').textContent;
cardHeader.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">${customerName}</h5>
<small class="text-muted">${customerEmail}</small>
</div>
<div>
<a href="/customer/edit/${customerId}?ref=customers-licenses${window.location.search ? '&' + window.location.search.substring(1) : ''}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal(${customerId})">
<i class="bi bi-plus"></i> Neue Lizenz
</button>
</div>
</div>`;
updateLicenseView(customerId, data.licenses);
}
})
.catch(error => {
console.error('Error:', error);
container.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Lizenzen</div>';
});
}
// Aktualisiere Lizenzansicht
function updateLicenseView(customerId, licenses) {
currentCustomerId = customerId;
const container = document.getElementById('licenseContainer');
if (licenses.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
<button class="btn btn-success" onclick="showNewLicenseModal(${customerId})">
<i class="bi bi-plus"></i> Erste Lizenz erstellen
</button>
</div>`;
return;
}
let html = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Typ</th>
<th>Gültig von</th>
<th>Gültig bis</th>
<th>Status</th>
<th>Ressourcen</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>`;
licenses.forEach(license => {
const statusClass = license.status === 'aktiv' ? 'bg-success' :
license.status === 'läuft bald ab' ? 'bg-warning' :
license.status === 'abgelaufen' ? 'bg-danger' : 'bg-secondary';
const typeClass = license.license_type === 'full' ? 'bg-primary' : 'bg-secondary';
html += `
<tr>
<td>
<code>${license.license_key}</code>
<button class="btn btn-sm btn-link" onclick="copyToClipboard('${license.license_key}')">
<i class="bi bi-clipboard"></i>
</button>
</td>
<td><span class="badge ${typeClass}">${license.license_type.toUpperCase()}</span></td>
<td>${license.valid_from || '-'}</td>
<td>${license.valid_until || '-'}</td>
<td><span class="badge ${statusClass}">${license.status}</span></td>
<td>
${license.domain_count > 0 ? '🌐 ' + license.domain_count : ''}
${license.ipv4_count > 0 ? '📡 ' + license.ipv4_count : ''}
${license.phone_count > 0 ? '📱 ' + license.phone_count : ''}
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})">
<i class="bi bi-power"></i>
</button>
<a href="/license/edit/${license.id}${window.location.search ? '?ref=customers-licenses&' + window.location.search.substring(1) : ''}" class="btn btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
// Toggle Lizenzstatus
function toggleLicenseStatus(licenseId, currentStatus) {
const newStatus = !currentStatus;
fetch(`/api/license/${licenseId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ is_active: newStatus })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload current customer licenses
if (currentCustomerId) {
loadCustomerLicenses(currentCustomerId);
}
}
})
.catch(error => console.error('Error:', error));
}
// Direkt zur Lizenzerstellung navigieren
function showNewLicenseModal(customerId) {
window.location.href = `/create?customer_id=${customerId}${window.location.search ? '&' + window.location.search.substring(1) : ''}`;
}
// Copy to clipboard
function copyToClipboard(text) {
const button = event.currentTarget;
navigator.clipboard.writeText(text).then(() => {
// Zeige kurz Feedback
button.innerHTML = '<i class="bi bi-check"></i>';
setTimeout(() => {
button.innerHTML = '<i class="bi bi-clipboard"></i>';
}, 1000);
}).catch(err => {
console.error('Fehler beim Kopieren:', err);
alert('Konnte nicht in die Zwischenablage kopieren');
});
}
// Toggle Testkunden
function toggleTestCustomers() {
const showTest = document.getElementById('showTestCustomers').checked;
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('show_test', showTest);
window.location.href = currentUrl.toString();
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.target.id === 'customerSearch') return; // Nicht bei Suche
const activeItem = document.querySelector('.customer-item.active');
if (!activeItem) return;
let targetItem = null;
if (e.key === 'ArrowUp') {
targetItem = activeItem.previousElementSibling;
} else if (e.key === 'ArrowDown') {
targetItem = activeItem.nextElementSibling;
}
if (targetItem && targetItem.classList.contains('customer-item')) {
e.preventDefault();
const customerId = parseInt(targetItem.dataset.customerId);
loadCustomerLicenses(customerId);
}
});
</script>
{% endblock %}

Datei anzeigen

@@ -0,0 +1,491 @@
{% extends "base.html" %}
{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Kunden & Lizenzen</h2>
<div>
<a href="/create" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Neue Lizenz
</a>
<a href="/batch" class="btn btn-primary">
<i class="bi bi-stack"></i> Batch-Lizenzen
</a>
<div class="btn-group">
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-download"></i> Export
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/export/licenses?format=excel"><i class="bi bi-file-earmark-excel"></i> Excel</a></li>
<li><a class="dropdown-item" href="/export/licenses?format=csv"><i class="bi bi-file-earmark-csv"></i> CSV</a></li>
</ul>
</div>
<a href="/" class="btn btn-secondary">
<i class="bi bi-house"></i> Dashboard
</a>
</div>
</div>
<div class="row">
<!-- Kundenliste (Links) -->
<div class="col-md-4 col-lg-3">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-people"></i> Kunden
<span class="badge bg-secondary float-end">{{ customers|length if customers else 0 }}</span>
</h5>
</div>
<div class="card-body p-0">
<!-- Suchfeld -->
<div class="p-3 border-bottom">
<input type="text" class="form-control mb-2" id="customerSearch"
placeholder="Kunde suchen..." autocomplete="off">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showTestCustomers"
{% if request.args.get('show_test', 'false').lower() == 'true' %}checked{% endif %}
onchange="toggleTestCustomers()">
<label class="form-check-label" for="showTestCustomers">
<small class="text-muted">Testkunden anzeigen</small>
</label>
</div>
</div>
<!-- Kundenliste -->
<div class="customer-list" style="max-height: 600px; overflow-y: auto;">
{% if customers %}
{% for customer in customers %}
<div class="customer-item p-3 border-bottom {% if customer[0] == selected_customer_id %}active{% endif %}"
data-customer-id="{{ customer[0] }}"
data-customer-name="{{ customer[1]|lower }}"
data-customer-email="{{ customer[2]|lower }}"
onclick="loadCustomerLicenses({{ customer[0] }})"
style="cursor: pointer;">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">{{ customer[1] }}</h6>
<small class="text-muted">{{ customer[2] }}</small>
</div>
<div class="text-end">
<span class="badge bg-primary">{{ customer[4] }}</span>
{% if customer[5] > 0 %}
<span class="badge bg-success">{{ customer[5] }}</span>
{% endif %}
{% if customer[6] > 0 %}
<span class="badge bg-danger">{{ customer[6] }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="p-4 text-center text-muted">
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3 mb-2">Keine Kunden vorhanden</p>
<small class="d-block mb-3">Erstellen Sie eine neue Lizenz, um automatisch einen Kunden anzulegen.</small>
<a href="/create" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle"></i> Neue Lizenz erstellen
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Lizenzdetails (Rechts) -->
<div class="col-md-8 col-lg-9">
<div class="card">
<div class="card-header bg-light">
{% if selected_customer %}
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">{{ selected_customer[1] }}</h5>
<small class="text-muted">{{ selected_customer[2] }}</small>
</div>
<div>
<a href="/customer/edit/{{ selected_customer[0] }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
<i class="bi bi-plus"></i> Neue Lizenz
</button>
</div>
</div>
{% else %}
<h5 class="mb-0">Wählen Sie einen Kunden aus</h5>
{% endif %}
</div>
<div class="card-body">
<div id="licenseContainer">
{% if selected_customer %}
{% if licenses %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Typ</th>
<th>Gültig von</th>
<th>Gültig bis</th>
<th>Status</th>
<th>Ressourcen</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for license in licenses %}
<tr>
<td>
<code>{{ license[1] }}</code>
<button class="btn btn-sm btn-link" onclick="copyToClipboard('{{ license[1] }}')">
<i class="bi bi-clipboard"></i>
</button>
</td>
<td>
<span class="badge {% if license[2] == 'full' %}bg-primary{% else %}bg-secondary{% endif %}">
{{ license[2]|upper }}
</span>
</td>
<td>{{ license[3].strftime('%d.%m.%Y') if license[3] else '-' }}</td>
<td>{{ license[4].strftime('%d.%m.%Y') if license[4] else '-' }}</td>
<td>
<span class="badge
{% if license[6] == 'aktiv' %}bg-success
{% elif license[6] == 'läuft bald ab' %}bg-warning
{% elif license[6] == 'abgelaufen' %}bg-danger
{% else %}bg-secondary{% endif %}">
{{ license[6] }}
</span>
</td>
<td>
{% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %}
{% if license[8] > 0 %}📡 {{ license[8] }}{% endif %}
{% if license[9] > 0 %}📱 {{ license[9] }}{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus({{ license[0] }}, {{ license[5] }})">
<i class="bi bi-power"></i>
</button>
<a href="/license/edit/{{ license[0] }}" class="btn btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
<button class="btn btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
<i class="bi bi-plus"></i> Erste Lizenz erstellen
</button>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-arrow-left text-muted" style="font-size: 3rem;"></i>
<p class="text-muted mt-3">Wählen Sie einen Kunden aus der Liste aus</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal für neue Lizenz -->
<div class="modal fade" id="newLicenseModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Neue Lizenz erstellen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Möchten Sie eine neue Lizenz für <strong id="modalCustomerName"></strong> erstellen?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-success" id="createLicenseBtn">Zur Lizenzerstellung</button>
</div>
</div>
</div>
</div>
<style>
.customer-item {
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.customer-item:hover {
background-color: #f8f9fa;
border-left-color: #dee2e6;
}
.customer-item.active {
background-color: #e7f3ff;
border-left-color: #0d6efd;
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
// Globale Variablen und Funktionen
let currentCustomerId = {{ selected_customer_id or 'null' }};
// Lade Lizenzen eines Kunden
function loadCustomerLicenses(customerId) {
const searchTerm = e.target.value.toLowerCase();
const customerItems = document.querySelectorAll('.customer-item');
customerItems.forEach(item => {
const name = item.dataset.customerName;
const email = item.dataset.customerEmail;
if (name.includes(searchTerm) || email.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
// Aktiven Status aktualisieren
document.querySelectorAll('.customer-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-customer-id="${customerId}"]`).classList.add('active');
// URL aktualisieren ohne Reload (behalte show_test Parameter)
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('customer_id', customerId);
window.history.pushState({}, '', currentUrl.toString());
// Lade Lizenzen via AJAX
const container = document.getElementById('licenseContainer');
const cardHeader = document.querySelector('.card-header.bg-light');
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
fetch(`/api/customer/${customerId}/licenses`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Update header with customer info
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
const customerName = customerItem.querySelector('h6').textContent;
const customerEmail = customerItem.querySelector('small').textContent;
cardHeader.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">${customerName}</h5>
<small class="text-muted">${customerEmail}</small>
</div>
<div>
<a href="/customer/edit/${customerId}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal(${customerId})">
<i class="bi bi-plus"></i> Neue Lizenz
</button>
</div>
</div>`;
updateLicenseView(customerId, data.licenses);
}
})
.catch(error => {
console.error('Error:', error);
container.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Lizenzen</div>';
});
}
// Aktualisiere Lizenzansicht
function updateLicenseView(customerId, licenses) {
currentCustomerId = customerId;
const container = document.getElementById('licenseContainer');
if (licenses.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
<button class="btn btn-success" onclick="showNewLicenseModal(${customerId})">
<i class="bi bi-plus"></i> Erste Lizenz erstellen
</button>
</div>`;
return;
}
let html = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Typ</th>
<th>Gültig von</th>
<th>Gültig bis</th>
<th>Status</th>
<th>Ressourcen</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>`;
licenses.forEach(license => {
const statusClass = license.status === 'aktiv' ? 'bg-success' :
license.status === 'läuft bald ab' ? 'bg-warning' :
license.status === 'abgelaufen' ? 'bg-danger' : 'bg-secondary';
const typeClass = license.license_type === 'full' ? 'bg-primary' : 'bg-secondary';
html += `
<tr>
<td>
<code>${license.license_key}</code>
<button class="btn btn-sm btn-link" onclick="copyToClipboard('${license.license_key}')">
<i class="bi bi-clipboard"></i>
</button>
</td>
<td><span class="badge ${typeClass}">${license.license_type.toUpperCase()}</span></td>
<td>${license.valid_from || '-'}</td>
<td>${license.valid_until || '-'}</td>
<td><span class="badge ${statusClass}">${license.status}</span></td>
<td>
${license.domain_count > 0 ? '🌐 ' + license.domain_count : ''}
${license.ipv4_count > 0 ? '📡 ' + license.ipv4_count : ''}
${license.phone_count > 0 ? '📱 ' + license.phone_count : ''}
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})">
<i class="bi bi-power"></i>
</button>
<a href="/license/edit/${license.id}" class="btn btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
// Toggle Lizenzstatus
function toggleLicenseStatus(licenseId, currentStatus) {
const newStatus = !currentStatus;
fetch(`/api/license/${licenseId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ is_active: newStatus })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload current customer licenses
if (currentCustomerId) {
loadCustomerLicenses(currentCustomerId);
}
}
})
.catch(error => console.error('Error:', error));
}
// Zeige Modal für neue Lizenz
function showNewLicenseModal(customerId) {
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
if (!customerItem) {
console.error('Kunde nicht gefunden:', customerId);
return;
}
const customerName = customerItem.querySelector('h6').textContent;
document.getElementById('modalCustomerName').textContent = customerName;
document.getElementById('createLicenseBtn').onclick = function() {
window.location.href = `/create?customer_id=${customerId}`;
};
// Check if bootstrap is loaded
if (typeof bootstrap === 'undefined') {
console.error('Bootstrap nicht geladen!');
// Fallback: Direkt zur Erstellung
if (confirm(`Neue Lizenz für ${customerName} erstellen?`)) {
window.location.href = `/create?customer_id=${customerId}`;
}
return;
}
const modalElement = document.getElementById('newLicenseModal');
const modal = new bootstrap.Modal(modalElement);
modal.show();
}
// Copy to clipboard
function copyToClipboard(text) {
const button = event.currentTarget;
navigator.clipboard.writeText(text).then(() => {
// Zeige kurz Feedback
button.innerHTML = '<i class="bi bi-check"></i>';
setTimeout(() => {
button.innerHTML = '<i class="bi bi-clipboard"></i>';
}, 1000);
}).catch(err => {
console.error('Fehler beim Kopieren:', err);
alert('Konnte nicht in die Zwischenablage kopieren');
});
}
// Toggle Testkunden
function toggleTestCustomers() {
const showTest = document.getElementById('showTestCustomers').checked;
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('show_test', showTest);
window.location.href = currentUrl.toString();
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.target.id === 'customerSearch') return; // Nicht bei Suche
const activeItem = document.querySelector('.customer-item.active');
if (!activeItem) return;
let targetItem = null;
if (e.key === 'ArrowUp') {
targetItem = activeItem.previousElementSibling;
} else if (e.key === 'ArrowDown') {
targetItem = activeItem.nextElementSibling;
}
if (targetItem && targetItem.classList.contains('customer-item')) {
e.preventDefault();
const customerId = parseInt(targetItem.dataset.customerId);
loadCustomerLicenses(customerId);
}
});
}); // Ende DOMContentLoaded
</script>
{% endblock %}

Datei anzeigen

@@ -68,6 +68,7 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Dashboard</h1>
<div>
<a href="/customers-licenses" class="btn btn-success">👥 Kunden & Lizenzen</a>
<a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
<a href="/audit" class="btn btn-secondary">📝 Log</a>
@@ -77,7 +78,7 @@
<!-- Statistik-Karten -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<a href="/customers" class="text-decoration-none">
<a href="/customers-licenses" class="text-decoration-none">
<div class="card stat-card h-100">
<div class="card-body text-center">
<div class="card-icon text-primary">👥</div>
@@ -88,7 +89,7 @@
</a>
</div>
<div class="col-md-4">
<a href="/licenses" class="text-decoration-none">
<a href="/customers-licenses" class="text-decoration-none">
<div class="card stat-card h-100">
<div class="card-body text-center">
<div class="card-icon text-info">📋</div>

Datei anzeigen

@@ -8,13 +8,16 @@
<h2>Kunde bearbeiten</h2>
<div>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/customers" class="btn btn-secondary">👥 Zurück zur Übersicht</a>
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">👥 Zurück zur Übersicht</a>
</div>
</div>
<div class="card mb-4">
<div class="card-body">
<form method="post" action="/customer/edit/{{ customer[0] }}" accept-charset="UTF-8">
{% if request.args.get('show_test') == 'true' %}
<input type="hidden" name="show_test" value="true">
{% endif %}
<div class="row g-3">
<div class="col-md-6">
<label for="name" class="form-label">Kundenname</label>
@@ -31,7 +34,7 @@
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="isTest" name="is_test" {% if customer[3] %}checked{% endif %}>
<input class="form-check-input" type="checkbox" id="isTest" name="is_test" {% if customer[4] %}checked{% endif %}>
<label class="form-check-label" for="isTest">
<i class="fas fa-flask"></i> Als Testdaten markieren
<small class="text-muted">(Kunde und seine Lizenzen werden von der Software ignoriert)</small>
@@ -40,7 +43,7 @@
<div class="mt-4">
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
<a href="/customers" class="btn btn-secondary">Abbrechen</a>
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>

Datei anzeigen

@@ -8,13 +8,16 @@
<h2>Lizenz bearbeiten</h2>
<div>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/licenses" class="btn btn-secondary">📋 Zurück zur Übersicht</a>
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">📋 Zurück zur Übersicht</a>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post" action="/license/edit/{{ license[0] }}" accept-charset="UTF-8">
{% if request.args.get('show_test') == 'true' %}
<input type="hidden" name="show_test" value="true">
{% endif %}
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Kunde</label>
@@ -64,7 +67,7 @@
<div class="mt-4">
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
<a href="/licenses" class="btn btn-secondary">Abbrechen</a>
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>

Datei anzeigen

@@ -317,6 +317,24 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Vorausgewählten Kunden setzen (falls von kombinierter Ansicht kommend)
{% if preselected_customer_id %}
// Lade Kundendetails und setze Auswahl
fetch('/api/customers?id={{ preselected_customer_id }}')
.then(response => response.json())
.then(data => {
if (data.results && data.results.length > 0) {
const customer = data.results[0];
// Erstelle Option und setze sie als ausgewählt
const option = new Option(customer.text, customer.id, true, true);
$('#customerSelect').append(option).trigger('change');
// Verstecke die Eingabefelder
document.getElementById('customerNameDiv').style.display = 'none';
document.getElementById('emailDiv').style.display = 'none';
}
});
{% endif %}
// Event Handler für Kundenauswahl
$('#customerSelect').on('select2:select', function (e) {
const selectedValue = e.params.data.id;

Datei anzeigen

@@ -31,6 +31,9 @@
<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>