Kunden & Lizenzen + Ressourcen - Nicht so zufrieden
Dieser Commit ist enthalten in:
@@ -2322,6 +2322,37 @@ def api_customer_licenses(customer_id):
|
||||
|
||||
licenses = []
|
||||
for row in cur.fetchall():
|
||||
license_id = row[0]
|
||||
|
||||
# Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
|
||||
cur.execute("""
|
||||
SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
|
||||
FROM resource_pools rp
|
||||
JOIN license_resources lr ON rp.id = lr.resource_id
|
||||
WHERE lr.license_id = %s AND lr.is_active = true
|
||||
ORDER BY rp.resource_type, rp.resource_value
|
||||
""", (license_id,))
|
||||
|
||||
resources = {
|
||||
'domains': [],
|
||||
'ipv4s': [],
|
||||
'phones': []
|
||||
}
|
||||
|
||||
for res_row in cur.fetchall():
|
||||
resource_info = {
|
||||
'id': res_row[0],
|
||||
'value': res_row[2],
|
||||
'assigned_at': res_row[3].strftime('%d.%m.%Y') if res_row[3] else ''
|
||||
}
|
||||
|
||||
if res_row[1] == 'domain':
|
||||
resources['domains'].append(resource_info)
|
||||
elif res_row[1] == 'ipv4':
|
||||
resources['ipv4s'].append(resource_info)
|
||||
elif res_row[1] == 'phone':
|
||||
resources['phones'].append(resource_info)
|
||||
|
||||
licenses.append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
@@ -2332,7 +2363,8 @@ def api_customer_licenses(customer_id):
|
||||
'status': row[6],
|
||||
'domain_count': row[7],
|
||||
'ipv4_count': row[8],
|
||||
'phone_count': row[9]
|
||||
'phone_count': row[9],
|
||||
'resources': resources
|
||||
})
|
||||
|
||||
cur.close()
|
||||
@@ -2447,6 +2479,56 @@ def api_license_quick_edit(license_id):
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route("/api/license/<int:license_id>/resources")
|
||||
@login_required
|
||||
def api_license_resources(license_id):
|
||||
"""API-Endpoint für detaillierte Ressourcen-Informationen einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
|
||||
cur.execute("""
|
||||
SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
|
||||
FROM resource_pools rp
|
||||
JOIN license_resources lr ON rp.id = lr.resource_id
|
||||
WHERE lr.license_id = %s AND lr.is_active = true
|
||||
ORDER BY rp.resource_type, rp.resource_value
|
||||
""", (license_id,))
|
||||
|
||||
resources = {
|
||||
'domains': [],
|
||||
'ipv4s': [],
|
||||
'phones': []
|
||||
}
|
||||
|
||||
for row in cur.fetchall():
|
||||
resource_info = {
|
||||
'id': row[0],
|
||||
'value': row[2],
|
||||
'assigned_at': row[3].strftime('%d.%m.%Y') if row[3] else ''
|
||||
}
|
||||
|
||||
if row[1] == 'domain':
|
||||
resources['domains'].append(resource_info)
|
||||
elif row[1] == 'ipv4':
|
||||
resources['ipv4s'].append(resource_info)
|
||||
elif row[1] == 'phone':
|
||||
resources['phones'].append(resource_info)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'resources': resources
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route("/sessions")
|
||||
@login_required
|
||||
def sessions():
|
||||
@@ -3228,7 +3310,8 @@ def resources():
|
||||
c.name as customer_name,
|
||||
rp.status_changed_at,
|
||||
rp.quarantine_reason,
|
||||
rp.quarantine_until
|
||||
rp.quarantine_until,
|
||||
c.id as customer_id
|
||||
FROM resource_pools rp
|
||||
LEFT JOIN licenses l ON rp.allocated_to_license = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
|
||||
@@ -155,9 +155,23 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %}
|
||||
{% if license[8] > 0 %}📡 {{ license[8] }}{% endif %}
|
||||
{% if license[9] > 0 %}📱 {{ license[9] }}{% endif %}
|
||||
<div class="resource-info">
|
||||
{% if license[7] > 0 %}
|
||||
<div class="d-inline-block me-2" data-bs-toggle="tooltip" title="Domains">
|
||||
🌐 {{ license[7] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if license[8] > 0 %}
|
||||
<div class="d-inline-block me-2" data-bs-toggle="tooltip" title="IPv4-Adressen">
|
||||
📡 {{ license[8] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if license[9] > 0 %}
|
||||
<div class="d-inline-block" data-bs-toggle="tooltip" title="Telefonnummern">
|
||||
📱 {{ license[9] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
@@ -196,7 +210,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal für neue Lizenz -->
|
||||
<!-- 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-lg">
|
||||
<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">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveResourceChanges()">
|
||||
<i class="bi bi-save"></i> Änderungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.customer-item {
|
||||
@@ -217,6 +271,22 @@
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -333,8 +403,37 @@ function updateLicenseView(customerId, licenses) {
|
||||
|
||||
const typeClass = license.license_type === 'full' ? 'bg-primary' : 'bg-secondary';
|
||||
|
||||
// Erstelle Ressourcen-HTML mit Details
|
||||
let resourcesHtml = '';
|
||||
if (license.resources) {
|
||||
if (license.domain_count > 0 && license.resources.domains) {
|
||||
resourcesHtml += `<div class="resource-group mb-1">
|
||||
<span class="resource-icon" data-bs-toggle="tooltip" title="Domains">🌐 ${license.domain_count}</span>
|
||||
<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>`;
|
||||
}
|
||||
if (license.ipv4_count > 0 && license.resources.ipv4s) {
|
||||
resourcesHtml += `<div class="resource-group mb-1">
|
||||
<span class="resource-icon" data-bs-toggle="tooltip" title="IPv4-Adressen">📡 ${license.ipv4_count}</span>
|
||||
<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>`;
|
||||
}
|
||||
if (license.phone_count > 0 && license.resources.phones) {
|
||||
resourcesHtml += `<div class="resource-group mb-1">
|
||||
<span class="resource-icon" data-bs-toggle="tooltip" title="Telefonnummern">📱 ${license.phone_count}</span>
|
||||
<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>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<tr data-license-id="${license.id}">
|
||||
<td>
|
||||
<code>${license.license_key}</code>
|
||||
<button class="btn btn-sm btn-link" onclick="copyToClipboard('${license.license_key}')">
|
||||
@@ -345,17 +444,18 @@ function updateLicenseView(customerId, licenses) {
|
||||
<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 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})">
|
||||
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})" title="Status ändern">
|
||||
<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">
|
||||
<button class="btn btn-outline-info" 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>
|
||||
@@ -439,5 +539,239 @@ document.addEventListener('keydown', function(e) {
|
||||
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="/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;
|
||||
|
||||
// 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>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<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();
|
||||
}
|
||||
})
|
||||
.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');
|
||||
}
|
||||
|
||||
// Dummy-Funktion für Speichern (wird später implementiert)
|
||||
function saveResourceChanges() {
|
||||
alert('Diese Funktion wird noch implementiert');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -68,10 +68,10 @@
|
||||
<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>
|
||||
<a href="/customers-licenses{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}" class="btn btn-success">👥 Kunden & Lizenzen</a>
|
||||
<a href="/create{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}" class="btn btn-primary">➕ Neue Lizenz</a>
|
||||
<a href="/resources{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}" class="btn btn-info">📦 Resource Pool</a>
|
||||
<a href="/audit{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}" class="btn btn-secondary">📝 Log</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -274,7 +274,14 @@
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-server"></i> Resource Pool Status
|
||||
<a href="/resources{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}" class="btn btn-sm btn-light float-end">Verwalten →</a>
|
||||
<div class="float-end">
|
||||
<a href="/resources/add{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}" class="btn btn-sm btn-success me-1" title="Ressourcen hinzufügen">
|
||||
<i class="bi bi-plus"></i> Hinzufügen
|
||||
</a>
|
||||
<a href="/resources{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}" class="btn btn-sm btn-light">
|
||||
Verwalten →
|
||||
</a>
|
||||
</div>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -284,25 +291,46 @@
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3">
|
||||
<i class="fas fa-{{ 'globe' if type == 'domain' else ('network-wired' if type == 'ipv4' else 'phone') }} fa-2x text-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}"></i>
|
||||
<a href="/resources?type={{ type }}{{ '&show_test=true' if request.args.get('show_test') == 'true' else '' }}"
|
||||
class="text-decoration-none">
|
||||
<i class="fas fa-{{ 'globe' if type == 'domain' else ('network-wired' if type == 'ipv4' else 'phone') }} fa-2x text-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-1">{{ type|upper }}</h6>
|
||||
<h6 class="mb-1">
|
||||
<a href="/resources?type={{ type }}{{ '&show_test=true' if request.args.get('show_test') == 'true' else '' }}"
|
||||
class="text-decoration-none text-dark">{{ type|upper }}</a>
|
||||
</h6>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<strong>{{ data.available }}</strong> / {{ data.total }} verfügbar
|
||||
<strong class="text-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}">{{ data.available }}</strong> / {{ data.total }} verfügbar
|
||||
</span>
|
||||
<span class="badge bg-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}">
|
||||
{{ data.available_percent }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress mt-1" style="height: 6px;">
|
||||
<div class="progress mt-1" style="height: 8px;">
|
||||
<div class="progress-bar bg-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}"
|
||||
style="width: {{ data.available_percent }}%"></div>
|
||||
style="width: {{ data.available_percent }}%"
|
||||
data-bs-toggle="tooltip"
|
||||
title="{{ data.available }} von {{ data.total }} verfügbar"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-muted">
|
||||
<a href="/resources?type={{ type }}&status=allocated{{ '&show_test=true' if request.args.get('show_test') == 'true' else '' }}"
|
||||
class="text-decoration-none text-muted">
|
||||
{{ data.allocated }} zugeteilt
|
||||
</a>
|
||||
</small>
|
||||
{% if data.quarantine > 0 %}
|
||||
<small>
|
||||
<a href="/resources?type={{ type }}&status=quarantine{{ '&show_test=true' if request.args.get('show_test') == 'true' else '' }}"
|
||||
class="text-decoration-none text-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i> {{ data.quarantine }} in Quarantäne
|
||||
</a>
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{{ data.allocated }} zugeteilt | {{ data.quarantine }} in Quarantäne
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -318,9 +346,15 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if resource_warning %}
|
||||
<div class="alert alert-warning mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Warnung:</strong> {{ resource_warning }}
|
||||
<div class="alert alert-danger mt-3 mb-0 d-flex justify-content-between align-items-center" role="alert">
|
||||
<div>
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Kritisch:</strong> {{ resource_warning }}
|
||||
</div>
|
||||
<a href="/resources/add{{ '?show_test=true' if request.args.get('show_test') == 'true' else '' }}"
|
||||
class="btn btn-sm btn-danger">
|
||||
<i class="bi bi-plus"></i> Ressourcen auffüllen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -403,18 +403,89 @@ function checkResourceAvailability() {
|
||||
// Hilfsfunktion zur Anzeige der Verfügbarkeit
|
||||
function updateAvailabilityDisplay(elementId, available, requested) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.textContent = available;
|
||||
const container = element.parentElement;
|
||||
|
||||
// Verfügbarkeit mit Prozentanzeige
|
||||
const percent = Math.round((available / (available + requested + 50)) * 100);
|
||||
let statusHtml = `<strong>${available}</strong>`;
|
||||
|
||||
if (requested > 0 && available < requested) {
|
||||
element.classList.remove('text-success');
|
||||
element.classList.remove('text-success', 'text-warning');
|
||||
element.classList.add('text-danger');
|
||||
} else if (available < 50) {
|
||||
element.classList.remove('text-success', 'text-danger');
|
||||
element.classList.add('text-warning');
|
||||
statusHtml += ` <i class="bi bi-exclamation-triangle"></i>`;
|
||||
|
||||
// Füge Warnung hinzu
|
||||
if (!container.querySelector('.availability-warning')) {
|
||||
const warning = document.createElement('div');
|
||||
warning.className = 'availability-warning text-danger small mt-1';
|
||||
warning.innerHTML = `<i class="bi bi-x-circle"></i> Nicht genügend Ressourcen verfügbar!`;
|
||||
container.appendChild(warning);
|
||||
}
|
||||
} else {
|
||||
element.classList.remove('text-danger', 'text-warning');
|
||||
element.classList.add('text-success');
|
||||
// Entferne Warnung wenn vorhanden
|
||||
const warning = container.querySelector('.availability-warning');
|
||||
if (warning) warning.remove();
|
||||
|
||||
if (available < 20) {
|
||||
element.classList.remove('text-success');
|
||||
element.classList.add('text-danger');
|
||||
statusHtml += ` <span class="badge bg-danger">Kritisch</span>`;
|
||||
} else if (available < 50) {
|
||||
element.classList.remove('text-success', 'text-danger');
|
||||
element.classList.add('text-warning');
|
||||
statusHtml += ` <span class="badge bg-warning text-dark">Niedrig</span>`;
|
||||
} else {
|
||||
element.classList.remove('text-danger', 'text-warning');
|
||||
element.classList.add('text-success');
|
||||
statusHtml += ` <span class="badge bg-success">OK</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
element.innerHTML = statusHtml;
|
||||
|
||||
// Zeige Fortschrittsbalken
|
||||
updateResourceProgressBar(elementId.replace('Available', ''), available, requested);
|
||||
}
|
||||
|
||||
// Fortschrittsbalken für Ressourcen
|
||||
function updateResourceProgressBar(resourceType, available, requested) {
|
||||
const progressId = `${resourceType}Progress`;
|
||||
let progressBar = document.getElementById(progressId);
|
||||
|
||||
// Erstelle Fortschrittsbalken wenn nicht vorhanden
|
||||
if (!progressBar) {
|
||||
const container = document.querySelector(`#${resourceType}Available`).parentElement.parentElement;
|
||||
const progressDiv = document.createElement('div');
|
||||
progressDiv.className = 'mt-2';
|
||||
progressDiv.innerHTML = `
|
||||
<div class="progress" style="height: 20px;" id="${progressId}">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: 0%">
|
||||
<span class="progress-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(progressDiv);
|
||||
progressBar = document.getElementById(progressId);
|
||||
}
|
||||
|
||||
const total = available + requested;
|
||||
const availablePercent = total > 0 ? (available / total) * 100 : 100;
|
||||
const bar = progressBar.querySelector('.progress-bar');
|
||||
const text = progressBar.querySelector('.progress-text');
|
||||
|
||||
// Setze Farbe basierend auf Verfügbarkeit
|
||||
bar.classList.remove('bg-success', 'bg-warning', 'bg-danger');
|
||||
if (requested > 0 && available < requested) {
|
||||
bar.classList.add('bg-danger');
|
||||
} else if (availablePercent < 30) {
|
||||
bar.classList.add('bg-warning');
|
||||
} else {
|
||||
bar.classList.add('bg-success');
|
||||
}
|
||||
|
||||
// Animiere Fortschrittsbalken
|
||||
bar.style.width = `${availablePercent}%`;
|
||||
text.textContent = requested > 0 ? `${available} von ${total}` : `${available} verfügbar`;
|
||||
}
|
||||
|
||||
// Gesamtstatus der Ressourcen-Verfügbarkeit
|
||||
|
||||
@@ -175,6 +175,11 @@
|
||||
<p class="text-muted mb-0">Verwalten Sie Domains, IPs und Telefonnummern</p>
|
||||
</div>
|
||||
<div>
|
||||
{% if request.referrer and 'customers-licenses' in request.referrer %}
|
||||
<a href="{{ request.referrer }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Zurück zu Kunden
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('add_resources', show_test=show_test) }}" class="btn btn-success">
|
||||
➕ Ressourcen hinzufügen
|
||||
</a>
|
||||
@@ -184,6 +189,9 @@
|
||||
<a href="{{ url_for('resources_report', show_test=show_test) }}" class="btn btn-secondary">
|
||||
📄 Report
|
||||
</a>
|
||||
<a href="{{ url_for('dashboard', show_test=show_test) }}" class="btn btn-secondary">
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -366,12 +374,17 @@
|
||||
<td>
|
||||
{% if resource[5] %}
|
||||
<div>
|
||||
<a href="{{ url_for('edit_license', license_id=resource[4]) }}"
|
||||
<a href="{{ url_for('customers_licenses', customer_id=resource[10] if resource[10] else '', show_test=show_test) }}"
|
||||
class="text-decoration-none">
|
||||
<strong>{{ resource[5] }}</strong>
|
||||
</a>
|
||||
</div>
|
||||
<div class="small text-muted">{{ resource[6] }}</div>
|
||||
<div class="small text-muted">
|
||||
<a href="{{ url_for('edit_license', license_id=resource[4]) }}?ref=resources{{ '&show_test=true' if show_test else '' }}"
|
||||
class="text-decoration-none text-muted">
|
||||
{{ resource[6] }}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren