1153 Zeilen
49 KiB
HTML
1153 Zeilen
49 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %}
|
|
|
|
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2 class="mb-0">Kunden & Lizenzen</h2>
|
|
<div>
|
|
<!-- Export Buttons ohne Dropdown -->
|
|
<div class="btn-group" role="group">
|
|
<a href="{{ url_for('export.export_customers', format='excel') }}" class="btn btn-success btn-sm">
|
|
<i class="bi bi-file-earmark-excel"></i> Kunden Excel
|
|
</a>
|
|
<a href="{{ url_for('export.export_customers', format='csv') }}" class="btn btn-secondary btn-sm">
|
|
<i class="bi bi-file-earmark-text"></i> Kunden CSV
|
|
</a>
|
|
<a href="{{ url_for('export.export_licenses', format='excel') }}" class="btn btn-success btn-sm">
|
|
<i class="bi bi-file-earmark-excel"></i> Lizenzen Excel
|
|
</a>
|
|
<a href="{{ url_for('export.export_licenses', format='csv') }}" class="btn btn-secondary btn-sm">
|
|
<i class="bi bi-file-earmark-text"></i> Lizenzen CSV
|
|
</a>
|
|
</div>
|
|
</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="showFakeCustomers"
|
|
{% if request.args.get('show_fake', 'false').lower() == 'true' %}checked{% endif %}
|
|
onchange="toggleFakeCustomers()">
|
|
<label class="form-check-label" for="showFakeCustomers">
|
|
<small class="text-muted">Fake-Kunden 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"
|
|
data-customer-id="{{ customer.id }}"
|
|
data-customer-name="{{ customer.name|lower }}"
|
|
data-customer-email="{{ customer.email|lower }}"
|
|
onclick="loadCustomerLicenses({{ customer.id }})"
|
|
style="cursor: pointer;">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-1">{{ customer.name }}</h6>
|
|
<small class="text-muted">{{ customer.email }}</small>
|
|
</div>
|
|
<div class="text-end">
|
|
<span class="badge bg-primary">{{ customer.license_count }}</span>
|
|
{% if customer.active_licenses > 0 %}
|
|
<span class="badge bg-success">{{ customer.active_licenses }}</span>
|
|
{% endif %}
|
|
{% if customer.test_licenses > 0 %}
|
|
<span class="badge bg-danger">{{ customer.test_licenses }}</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="{{ url_for('licenses.create_license') }}" 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">
|
|
<h5 class="mb-0">Wählen Sie einen Kunden aus</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="licenseContainer">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal für Ressourcen-Details -->
|
|
<div class="modal fade" id="resourceDetailsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Ressourcen-Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="resourceDetailsContent">
|
|
<!-- Wird dynamisch gefüllt -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
|
<a href="#" id="goToResourcePool" class="btn btn-primary">
|
|
<i class="bi bi-box-arrow-up-right"></i> Zum Resource Pool
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal für Ressourcen-Verwaltung -->
|
|
<div class="modal fade" id="resourceManagementModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Ressourcen verwalten</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="resourceManagementContent">
|
|
<!-- Wird dynamisch gefüllt -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<div class="me-auto">
|
|
<span class="text-muted" id="selectionCounter">0 Ressourcen ausgewählt</span>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveResourceChanges()" id="saveResourcesBtn">
|
|
<i class="bi bi-save"></i> Änderungen speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal für Geräte-Verwaltung -->
|
|
<div class="modal fade" id="deviceManagementModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Geräte verwalten</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="deviceManagementContent">
|
|
<div class="text-center">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Lädt...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</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;
|
|
}
|
|
.resource-group {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
background-color: #f8f9fa;
|
|
border-radius: 4px;
|
|
margin-right: 4px;
|
|
}
|
|
.resource-icon {
|
|
font-size: 0.9rem;
|
|
}
|
|
.resources-cell {
|
|
min-width: 150px;
|
|
}
|
|
.btn-link {
|
|
padding: 0 4px;
|
|
}
|
|
|
|
/* Multi-select improvements */
|
|
select[multiple] {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #6c757d #f8f9fa;
|
|
}
|
|
|
|
select[multiple]::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
select[multiple]::-webkit-scrollbar-track {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
select[multiple]::-webkit-scrollbar-thumb {
|
|
background-color: #6c757d;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
select[multiple] option {
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
select[multiple] option:checked {
|
|
background-color: #0d6efd;
|
|
color: white;
|
|
}
|
|
|
|
select[multiple] option:hover {
|
|
background-color: #e9ecef;
|
|
color: #212529;
|
|
}
|
|
|
|
/* Modal improvements */
|
|
.modal-xl {
|
|
max-width: 1200px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// Globale Variablen
|
|
let currentCustomerId = 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_fake 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(`{{ url_for('customers.api_customer_licenses', customer_id=0) }}`.replace('0', customerId))
|
|
.then(response => {
|
|
console.log('API Response Status:', response.status);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('API Response Data:', 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);
|
|
} else {
|
|
console.error('API returned success: false', data);
|
|
container.innerHTML = '<div class="alert alert-warning">Keine Lizenzdaten gefunden</div>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
container.innerHTML = `<div class="alert alert-danger">Fehler beim Laden der Lizenzen: ${error.message}</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>Server 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';
|
|
|
|
// License Server Status
|
|
let serverStatusHtml = '';
|
|
if (license.recent_heartbeats > 0) {
|
|
serverStatusHtml = `<span class="badge bg-success" title="Aktiv - ${license.active_server_devices} Geräte">💚 Online</span>`;
|
|
if (license.unresolved_anomalies > 0) {
|
|
serverStatusHtml += `<br><span class="badge bg-danger" title="${license.unresolved_anomalies} ungelöste Anomalien">⚠️ ${license.unresolved_anomalies}</span>`;
|
|
}
|
|
} else if (license.last_heartbeat) {
|
|
const lastSeen = new Date(license.last_heartbeat);
|
|
const minutesAgo = Math.floor((new Date() - lastSeen) / 60000);
|
|
if (minutesAgo < 60) {
|
|
serverStatusHtml = `<span class="badge bg-warning" title="Zuletzt vor ${minutesAgo} Min">⏱️ ${minutesAgo} Min</span>`;
|
|
} else {
|
|
const hoursAgo = Math.floor(minutesAgo / 60);
|
|
serverStatusHtml = `<span class="badge bg-secondary" title="Zuletzt vor ${hoursAgo}h">💤 Offline</span>`;
|
|
}
|
|
} else {
|
|
serverStatusHtml = `<span class="badge bg-secondary">-</span>`;
|
|
}
|
|
|
|
// Erstelle Ressourcen-HTML mit Details
|
|
let resourcesHtml = '';
|
|
const actualDomainCount = license.actual_domain_count || 0;
|
|
const actualIpv4Count = license.actual_ipv4_count || 0;
|
|
const actualPhoneCount = license.actual_phone_count || 0;
|
|
|
|
// Domains - immer anzeigen
|
|
resourcesHtml += `<div class="resource-group mb-1">
|
|
<span class="resource-icon" data-bs-toggle="tooltip" title="Domains">🌐 ${actualDomainCount}</span>
|
|
${actualDomainCount > 0 ? `<button class="btn btn-sm btn-link p-0 ms-1" onclick="showResourceDetails(${license.id}, 'domain')">
|
|
<i class="bi bi-info-circle"></i>
|
|
</button>` : ''}
|
|
</div>`;
|
|
|
|
// IPv4 - immer anzeigen
|
|
resourcesHtml += `<div class="resource-group mb-1">
|
|
<span class="resource-icon" data-bs-toggle="tooltip" title="IPv4-Adressen">📡 ${actualIpv4Count}</span>
|
|
${actualIpv4Count > 0 ? `<button class="btn btn-sm btn-link p-0 ms-1" onclick="showResourceDetails(${license.id}, 'ipv4')">
|
|
<i class="bi bi-info-circle"></i>
|
|
</button>` : ''}
|
|
</div>`;
|
|
|
|
// Telefonnummern - immer anzeigen
|
|
resourcesHtml += `<div class="resource-group mb-1">
|
|
<span class="resource-icon" data-bs-toggle="tooltip" title="Telefonnummern">📱 ${actualPhoneCount}</span>
|
|
${actualPhoneCount > 0 ? `<button class="btn btn-sm btn-link p-0 ms-1" onclick="showResourceDetails(${license.id}, 'phone')">
|
|
<i class="bi bi-info-circle"></i>
|
|
</button>` : ''}
|
|
</div>`;
|
|
|
|
// Geräte-Anzeige hinzufügen (mit Limit, da Geräte limitiert sind)
|
|
resourcesHtml += `<div class="resource-group">
|
|
<span class="resource-icon" data-bs-toggle="tooltip" title="Geräte (aktiv/limit)">💻 ${license.active_devices}/${license.device_limit}</span>
|
|
</div>`;
|
|
|
|
html += `
|
|
<tr data-license-id="${license.id}">
|
|
<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>${serverStatusHtml}</td>
|
|
<td class="resources-cell">
|
|
${resourcesHtml || '<span class="text-muted">-</span>'}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})" title="Status ändern">
|
|
<i class="bi bi-power"></i>
|
|
</button>
|
|
<button class="btn btn-outline-info" onclick="showDeviceManagement(${license.id})" title="Geräte verwalten">
|
|
<i class="bi bi-laptop"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="showResourceManagement(${license.id})" title="Ressourcen verwalten">
|
|
<i class="bi bi-gear"></i>
|
|
</button>
|
|
<a href="/license/edit/${license.id}${window.location.search ? '?ref=customers-licenses&' + window.location.search.substring(1) : ''}" class="btn btn-outline-secondary" title="Bearbeiten">
|
|
<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 = `{{ url_for('licenses.create_license') }}?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 toggleFakeCustomers() {
|
|
const showTest = document.getElementById('showFakeCustomers').checked;
|
|
const currentUrl = new URL(window.location);
|
|
currentUrl.searchParams.set('show_fake', 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);
|
|
}
|
|
});
|
|
|
|
// Globale Variable für aktuelle Lizenz-Daten
|
|
let currentLicenses = {};
|
|
|
|
// Erweitere updateLicenseView um Daten zu speichern
|
|
const originalUpdateLicenseView = updateLicenseView;
|
|
updateLicenseView = function(customerId, licenses) {
|
|
// Speichere Lizenzdaten für späteren Zugriff
|
|
licenses.forEach(license => {
|
|
currentLicenses[license.id] = license;
|
|
});
|
|
originalUpdateLicenseView(customerId, licenses);
|
|
|
|
// Bootstrap Tooltips initialisieren
|
|
setTimeout(() => {
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
});
|
|
}, 100);
|
|
};
|
|
|
|
// Zeige Ressourcen-Details
|
|
function showResourceDetails(licenseId, resourceType) {
|
|
const license = currentLicenses[licenseId];
|
|
if (!license || !license.resources) return;
|
|
|
|
let resources = [];
|
|
let title = '';
|
|
let icon = '';
|
|
|
|
switch(resourceType) {
|
|
case 'domain':
|
|
resources = license.resources.domains;
|
|
title = 'Domains';
|
|
icon = '🌐';
|
|
break;
|
|
case 'ipv4':
|
|
resources = license.resources.ipv4s;
|
|
title = 'IPv4-Adressen';
|
|
icon = '📡';
|
|
break;
|
|
case 'phone':
|
|
resources = license.resources.phones;
|
|
title = 'Telefonnummern';
|
|
icon = '📱';
|
|
break;
|
|
}
|
|
|
|
// Modal-Inhalt generieren
|
|
let content = `
|
|
<h6>${icon} ${title} (${resources.length})</h6>
|
|
<div class="table-responsive mt-3">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Ressource</th>
|
|
<th>Zugewiesen am</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>`;
|
|
|
|
resources.forEach(resource => {
|
|
content += `
|
|
<tr>
|
|
<td>
|
|
<code>${resource.value}</code>
|
|
<button class="btn btn-sm btn-link" onclick="copyToClipboard('${resource.value}')">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
</td>
|
|
<td>${resource.assigned_at}</td>
|
|
<td>
|
|
<a href="{{ url_for('resources.resources') }}?search=${encodeURIComponent(resource.value)}"
|
|
class="btn btn-sm btn-outline-primary" target="_blank">
|
|
<i class="bi bi-box-arrow-up-right"></i> Details
|
|
</a>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
|
|
content += `
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
|
|
// Link zum Resource Pool vorbereiten
|
|
document.getElementById('goToResourcePool').href = `/resources?type=${resourceType}&status=allocated`;
|
|
|
|
// Modal zeigen
|
|
document.getElementById('resourceDetailsContent').innerHTML = content;
|
|
const modal = new bootstrap.Modal(document.getElementById('resourceDetailsModal'));
|
|
modal.show();
|
|
}
|
|
|
|
// Ressourcen-Verwaltung Modal
|
|
function showResourceManagement(licenseId) {
|
|
const license = currentLicenses[licenseId];
|
|
if (!license) return;
|
|
|
|
// Speichere License-ID im Modal für späteren Zugriff
|
|
document.getElementById('resourceManagementModal').dataset.licenseId = licenseId;
|
|
|
|
// Lade aktuelle Ressourcen-Informationen
|
|
fetch(`/api/license/${licenseId}/resources`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
let content = `
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle"></i> Hier können Sie die Ressourcen dieser Lizenz verwalten.
|
|
</div>
|
|
|
|
<h6>Aktuelle Ressourcen</h6>
|
|
<div class="row g-3 mb-4">`;
|
|
|
|
// Domains
|
|
if (license.domain_count > 0) {
|
|
content += `
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">🌐 Domains (${license.domain_count})</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="list-unstyled mb-0">`;
|
|
|
|
data.resources.domains.forEach(domain => {
|
|
content += `
|
|
<li class="mb-2">
|
|
<code>${domain.value}</code>
|
|
<button class="btn btn-sm btn-link text-danger float-end"
|
|
onclick="quarantineResource(${domain.id}, '${domain.value}')">
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
</button>
|
|
</li>`;
|
|
});
|
|
|
|
content += `
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// IPv4s
|
|
if (license.ipv4_count > 0) {
|
|
content += `
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">📡 IPv4-Adressen (${license.ipv4_count})</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="list-unstyled mb-0">`;
|
|
|
|
data.resources.ipv4s.forEach(ipv4 => {
|
|
content += `
|
|
<li class="mb-2">
|
|
<code>${ipv4.value}</code>
|
|
<button class="btn btn-sm btn-link text-danger float-end"
|
|
onclick="quarantineResource(${ipv4.id}, '${ipv4.value}')">
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
</button>
|
|
</li>`;
|
|
});
|
|
|
|
content += `
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// Phones
|
|
if (license.phone_count > 0) {
|
|
content += `
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">📱 Telefonnummern (${license.phone_count})</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="list-unstyled mb-0">`;
|
|
|
|
data.resources.phones.forEach(phone => {
|
|
content += `
|
|
<li class="mb-2">
|
|
<code>${phone.value}</code>
|
|
<button class="btn btn-sm btn-link text-danger float-end"
|
|
onclick="quarantineResource(${phone.id}, '${phone.value}')">
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
</button>
|
|
</li>`;
|
|
});
|
|
|
|
content += `
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
content += `
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h6>Neue Ressourcen zuweisen</h6>
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle"></i> Wählen Sie verfügbare Ressourcen aus, um sie dieser Lizenz zuzuweisen.
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">🌐 Domains hinzufügen</label>
|
|
<div class="text-muted small mb-2">Mehrfachauswahl mit Strg/Cmd + Klick</div>
|
|
<select id="availableDomains" class="form-select" size="10" multiple>
|
|
<option value="" disabled>Lade verfügbare Domains...</option>
|
|
</select>
|
|
<div class="mt-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllInCategory('availableDomains')">
|
|
Alle auswählen
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="deselectAllInCategory('availableDomains')">
|
|
Auswahl aufheben
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">📡 IPv4-Adressen hinzufügen</label>
|
|
<div class="text-muted small mb-2">Mehrfachauswahl mit Strg/Cmd + Klick</div>
|
|
<select id="availableIpv4s" class="form-select" size="10" multiple>
|
|
<option value="" disabled>Lade verfügbare IPs...</option>
|
|
</select>
|
|
<div class="mt-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllInCategory('availableIpv4s')">
|
|
Alle auswählen
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="deselectAllInCategory('availableIpv4s')">
|
|
Auswahl aufheben
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">📱 Telefonnummern hinzufügen</label>
|
|
<div class="text-muted small mb-2">Mehrfachauswahl mit Strg/Cmd + Klick</div>
|
|
<select id="availablePhones" class="form-select" size="10" multiple>
|
|
<option value="" disabled>Lade verfügbare Nummern...</option>
|
|
</select>
|
|
<div class="mt-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllInCategory('availablePhones')">
|
|
Alle auswählen
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="deselectAllInCategory('availablePhones')">
|
|
Auswahl aufheben
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-warning mt-3">
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
Das Quarantäne-setzen einer Ressource entfernt sie von der Lizenz und markiert sie als problematisch.
|
|
</div>`;
|
|
|
|
document.getElementById('resourceManagementContent').innerHTML = content;
|
|
const modal = new bootstrap.Modal(document.getElementById('resourceManagementModal'));
|
|
modal.show();
|
|
|
|
// Lade verfügbare Ressourcen
|
|
loadAvailableResources(licenseId);
|
|
|
|
// Add event listeners for selection changes after resources are loaded
|
|
setTimeout(() => {
|
|
['availableDomains', 'availableIpv4s', 'availablePhones'].forEach(selectId => {
|
|
const select = document.getElementById(selectId);
|
|
if (select) {
|
|
select.addEventListener('change', updateSelectionCounter);
|
|
}
|
|
});
|
|
updateSelectionCounter();
|
|
}, 500);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading resources:', error);
|
|
alert('Fehler beim Laden der Ressourcen-Details');
|
|
});
|
|
}
|
|
|
|
// Ressource in Quarantäne setzen
|
|
function quarantineResource(resourceId, resourceValue) {
|
|
if (!confirm(`Möchten Sie die Ressource "${resourceValue}" wirklich in Quarantäne setzen?`)) {
|
|
return;
|
|
}
|
|
|
|
// Hier würde die API-Call für Quarantäne implementiert
|
|
alert('Diese Funktion wird noch implementiert');
|
|
}
|
|
|
|
// Lade verfügbare Ressourcen
|
|
function loadAvailableResources(licenseId) {
|
|
// Hole show_fake Parameter aus der URL
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const showTest = urlParams.get('show_fake') === 'true';
|
|
|
|
// Lade verfügbare Domains
|
|
fetch(`/api/resources/check-availability?type=domain&count=200&show_fake=${showTest}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const select = document.getElementById('availableDomains');
|
|
select.innerHTML = '';
|
|
if (data.available && data.available.length > 0) {
|
|
data.available.forEach(resource => {
|
|
const option = document.createElement('option');
|
|
option.value = resource.id;
|
|
option.textContent = resource.value;
|
|
select.appendChild(option);
|
|
});
|
|
// Zeige Anzahl verfügbarer Ressourcen
|
|
const label = select.parentElement.querySelector('.form-label');
|
|
label.innerHTML = `🌐 Domains hinzufügen (${data.available.length} verfügbar)`;
|
|
} else {
|
|
select.innerHTML = '<option value="" disabled>Keine verfügbaren Domains</option>';
|
|
}
|
|
});
|
|
|
|
// Lade verfügbare IPv4s
|
|
fetch(`/api/resources/check-availability?type=ipv4&count=200&show_fake=${showTest}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const select = document.getElementById('availableIpv4s');
|
|
select.innerHTML = '';
|
|
if (data.available && data.available.length > 0) {
|
|
data.available.forEach(resource => {
|
|
const option = document.createElement('option');
|
|
option.value = resource.id;
|
|
option.textContent = resource.value;
|
|
select.appendChild(option);
|
|
});
|
|
// Zeige Anzahl verfügbarer Ressourcen
|
|
const label = select.parentElement.querySelector('.form-label');
|
|
label.innerHTML = `📡 IPv4-Adressen hinzufügen (${data.available.length} verfügbar)`;
|
|
} else {
|
|
select.innerHTML = '<option value="" disabled>Keine verfügbaren IPv4-Adressen</option>';
|
|
}
|
|
});
|
|
|
|
// Lade verfügbare Telefonnummern
|
|
fetch(`/api/resources/check-availability?type=phone&count=200&show_fake=${showTest}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const select = document.getElementById('availablePhones');
|
|
select.innerHTML = '';
|
|
if (data.available && data.available.length > 0) {
|
|
data.available.forEach(resource => {
|
|
const option = document.createElement('option');
|
|
option.value = resource.id;
|
|
option.textContent = resource.value;
|
|
select.appendChild(option);
|
|
});
|
|
// Zeige Anzahl verfügbarer Ressourcen
|
|
const label = select.parentElement.querySelector('.form-label');
|
|
label.innerHTML = `📱 Telefonnummern hinzufügen (${data.available.length} verfügbar)`;
|
|
} else {
|
|
select.innerHTML = '<option value="" disabled>Keine verfügbaren Telefonnummern</option>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Alle Optionen in einer Kategorie auswählen
|
|
function selectAllInCategory(selectId) {
|
|
const select = document.getElementById(selectId);
|
|
for (let i = 0; i < select.options.length; i++) {
|
|
if (!select.options[i].disabled) {
|
|
select.options[i].selected = true;
|
|
}
|
|
}
|
|
updateSelectionCounter();
|
|
}
|
|
|
|
// Auswahl in einer Kategorie aufheben
|
|
function deselectAllInCategory(selectId) {
|
|
const select = document.getElementById(selectId);
|
|
for (let i = 0; i < select.options.length; i++) {
|
|
select.options[i].selected = false;
|
|
}
|
|
updateSelectionCounter();
|
|
}
|
|
|
|
// Update selection counter
|
|
function updateSelectionCounter() {
|
|
const domainsCount = document.getElementById('availableDomains').selectedOptions.length;
|
|
const ipv4sCount = document.getElementById('availableIpv4s').selectedOptions.length;
|
|
const phonesCount = document.getElementById('availablePhones').selectedOptions.length;
|
|
const totalCount = domainsCount + ipv4sCount + phonesCount;
|
|
|
|
const counter = document.getElementById('selectionCounter');
|
|
if (counter) {
|
|
counter.textContent = `${totalCount} Ressourcen ausgewählt`;
|
|
|
|
// Update save button state
|
|
const saveBtn = document.getElementById('saveResourcesBtn');
|
|
if (saveBtn) {
|
|
saveBtn.disabled = totalCount === 0;
|
|
if (totalCount === 0) {
|
|
saveBtn.classList.add('disabled');
|
|
} else {
|
|
saveBtn.classList.remove('disabled');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Speichere Ressourcen-Änderungen
|
|
function saveResourceChanges() {
|
|
const modal = document.getElementById('resourceManagementModal');
|
|
const licenseId = modal.dataset.licenseId || currentLicenses[Object.keys(currentLicenses)[0]].id;
|
|
|
|
// Sammle ausgewählte Ressourcen
|
|
const selectedDomains = Array.from(document.getElementById('availableDomains').selectedOptions).map(opt => opt.value);
|
|
const selectedIpv4s = Array.from(document.getElementById('availableIpv4s').selectedOptions).map(opt => opt.value);
|
|
const selectedPhones = Array.from(document.getElementById('availablePhones').selectedOptions).map(opt => opt.value);
|
|
|
|
const allResources = [...selectedDomains, ...selectedIpv4s, ...selectedPhones];
|
|
|
|
if (allResources.length === 0) {
|
|
alert('Bitte wählen Sie mindestens eine Ressource aus.');
|
|
return;
|
|
}
|
|
|
|
// Disable save button and show loading state
|
|
const saveBtn = document.getElementById('saveResourcesBtn');
|
|
const originalBtnText = saveBtn.innerHTML;
|
|
saveBtn.disabled = true;
|
|
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>Wird gespeichert...';
|
|
|
|
// API-Call zum Zuweisen der Ressourcen
|
|
fetch('{{ url_for('api.allocate_resources') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
license_id: licenseId,
|
|
resource_ids: allResources
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Show success message with details
|
|
const domainCount = selectedDomains.length;
|
|
const ipv4Count = selectedIpv4s.length;
|
|
const phoneCount = selectedPhones.length;
|
|
|
|
let message = `✅ ${data.allocated} Ressourcen erfolgreich zugewiesen!\n\n`;
|
|
if (domainCount > 0) message += `🌐 ${domainCount} Domains\n`;
|
|
if (ipv4Count > 0) message += `📡 ${ipv4Count} IPv4-Adressen\n`;
|
|
if (phoneCount > 0) message += `📱 ${phoneCount} Telefonnummern`;
|
|
|
|
alert(message);
|
|
|
|
// Modal schließen und Ansicht aktualisieren
|
|
bootstrap.Modal.getInstance(modal).hide();
|
|
if (currentCustomerId) {
|
|
loadCustomerLicenses(currentCustomerId);
|
|
}
|
|
} else {
|
|
alert('❌ ' + (data.message || 'Fehler beim Zuweisen der Ressourcen'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error allocating resources:', error);
|
|
alert('❌ Fehler beim Zuweisen der Ressourcen');
|
|
})
|
|
.finally(() => {
|
|
// Restore button state
|
|
saveBtn.disabled = false;
|
|
saveBtn.innerHTML = originalBtnText;
|
|
});
|
|
}
|
|
|
|
// Geräte-Verwaltung anzeigen
|
|
function showDeviceManagement(licenseId) {
|
|
const modal = new bootstrap.Modal(document.getElementById('deviceManagementModal'));
|
|
modal.show();
|
|
|
|
// Lade Geräte-Daten
|
|
fetch(`/api/license/${licenseId}/devices`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
let content = `
|
|
<div class="mb-3">
|
|
<div class="alert alert-info">
|
|
<strong>Gerätelimit:</strong> ${data.active_count} von ${data.device_limit} Geräten aktiv
|
|
</div>
|
|
</div>`;
|
|
|
|
if (data.devices.length === 0) {
|
|
content += `
|
|
<div class="text-center py-4">
|
|
<i class="bi bi-laptop text-muted" style="font-size: 3rem;"></i>
|
|
<p class="text-muted mt-3">Noch keine Geräte registriert</p>
|
|
</div>`;
|
|
} else {
|
|
content += `
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Gerätename</th>
|
|
<th>Hardware-ID</th>
|
|
<th>Betriebssystem</th>
|
|
<th>Erste Registrierung</th>
|
|
<th>Letzte Aktivität</th>
|
|
<th>IP-Adresse</th>
|
|
<th>Status</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>`;
|
|
|
|
data.devices.forEach(device => {
|
|
const statusBadge = device.is_active
|
|
? '<span class="badge bg-success">Aktiv</span>'
|
|
: '<span class="badge bg-secondary">Deaktiviert</span>';
|
|
|
|
const actionButton = device.is_active
|
|
? `<button class="btn btn-sm btn-danger" onclick="deactivateDevice(${licenseId}, ${device.id}, '${device.device_name}')">
|
|
<i class="bi bi-x-circle"></i> Deaktivieren
|
|
</button>`
|
|
: '<span class="text-muted">-</span>';
|
|
|
|
content += `
|
|
<tr>
|
|
<td>${device.device_name}</td>
|
|
<td><small class="text-muted">${device.hardware_id.substring(0, 12)}...</small></td>
|
|
<td>${device.operating_system}</td>
|
|
<td>${device.first_seen}</td>
|
|
<td>${device.last_seen}</td>
|
|
<td>${device.ip_address}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${actionButton}</td>
|
|
</tr>`;
|
|
});
|
|
|
|
content += `
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
document.getElementById('deviceManagementContent').innerHTML = content;
|
|
} else {
|
|
document.getElementById('deviceManagementContent').innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle"></i> ${data.message || 'Fehler beim Laden der Geräte'}
|
|
</div>`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading devices:', error);
|
|
document.getElementById('deviceManagementContent').innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle"></i> Fehler beim Laden der Geräte
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
// Gerät deaktivieren
|
|
function deactivateDevice(licenseId, deviceId, deviceName) {
|
|
if (!confirm(`Möchten Sie das Gerät "${deviceName}" wirklich deaktivieren?`)) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/license/${licenseId}/deactivate-device/${deviceId}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Aktualisiere die Geräte-Ansicht
|
|
showDeviceManagement(licenseId);
|
|
// Aktualisiere die Lizenz-Anzeige wenn möglich
|
|
if (typeof loadCustomerLicenses === 'function' && currentCustomerId) {
|
|
loadCustomerLicenses(currentCustomerId);
|
|
}
|
|
} else {
|
|
alert(data.message || 'Fehler beim Deaktivieren des Geräts');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error deactivating device:', error);
|
|
alert('Fehler beim Deaktivieren des Geräts');
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %} |