From c7decff64ea4caf06f0489b7f3caeb7eb7316c63 Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Mon, 9 Jun 2025 23:45:42 +0200 Subject: [PATCH] Kunden & Lizenzen + Ressourcen - Nicht so zufrieden --- v2/docker-compose.yaml | 2 - v2_adminpanel/app.py | 87 ++++- .../templates/customers_licenses.html | 356 +++++++++++++++++- v2_adminpanel/templates/dashboard.html | 66 +++- v2_adminpanel/templates/index.html | 85 ++++- v2_adminpanel/templates/resources.html | 17 +- 6 files changed, 573 insertions(+), 40 deletions(-) diff --git a/v2/docker-compose.yaml b/v2/docker-compose.yaml index f8b8932..5ab8942 100644 --- a/v2/docker-compose.yaml +++ b/v2/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.9" - services: postgres: build: diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 9dec360..8b0c3ad 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -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//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 diff --git a/v2_adminpanel/templates/customers_licenses.html b/v2_adminpanel/templates/customers_licenses.html index ebc4d68..d2df36e 100644 --- a/v2_adminpanel/templates/customers_licenses.html +++ b/v2_adminpanel/templates/customers_licenses.html @@ -155,9 +155,23 @@ - {% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %} - {% if license[8] > 0 %}📡 {{ license[8] }}{% endif %} - {% if license[9] > 0 %}📱 {{ license[9] }}{% endif %} +
+ {% if license[7] > 0 %} +
+ 🌐 {{ license[7] }} +
+ {% endif %} + {% if license[8] > 0 %} +
+ 📡 {{ license[8] }} +
+ {% endif %} + {% if license[9] > 0 %} +
+ 📱 {{ license[9] }} +
+ {% endif %} +
@@ -196,7 +210,47 @@
- + + + + + {% 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 += `
+ 🌐 ${license.domain_count} + +
`; + } + if (license.ipv4_count > 0 && license.resources.ipv4s) { + resourcesHtml += `
+ 📡 ${license.ipv4_count} + +
`; + } + if (license.phone_count > 0 && license.resources.phones) { + resourcesHtml += `
+ 📱 ${license.phone_count} + +
`; + } + } + html += ` - + ${license.license_key} - + + @@ -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 = ` +
${icon} ${title} (${resources.length})
+
+ + + + + + + + + `; + + resources.forEach(resource => { + content += ` + + + + + `; + }); + + content += ` + +
RessourceZugewiesen amAktionen
+ ${resource.value} + + ${resource.assigned_at} + + Details + +
+
`; + + // 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 = ` +
+ Hier können Sie die Ressourcen dieser Lizenz verwalten. +
+ +
Aktuelle Ressourcen
+
`; + + // Domains + if (license.domain_count > 0) { + content += ` +
+
+
+
🌐 Domains (${license.domain_count})
+
+
+
    `; + + data.resources.domains.forEach(domain => { + content += ` +
  • + ${domain.value} + +
  • `; + }); + + content += ` +
+
+
+
`; + } + + // IPv4s + if (license.ipv4_count > 0) { + content += ` +
+
+
+
📡 IPv4-Adressen (${license.ipv4_count})
+
+
+
    `; + + data.resources.ipv4s.forEach(ipv4 => { + content += ` +
  • + ${ipv4.value} + +
  • `; + }); + + content += ` +
+
+
+
`; + } + + // Phones + if (license.phone_count > 0) { + content += ` +
+
+
+
📱 Telefonnummern (${license.phone_count})
+
+
+
    `; + + data.resources.phones.forEach(phone => { + content += ` +
  • + ${phone.value} + +
  • `; + }); + + content += ` +
+
+
+
`; + } + + content += ` +
+ +
+ + Das Quarantäne-setzen einer Ressource entfernt sie von der Lizenz und markiert sie als problematisch. +
`; + + 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'); +} {% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/templates/dashboard.html b/v2_adminpanel/templates/dashboard.html index 0a62b9a..a13f308 100644 --- a/v2_adminpanel/templates/dashboard.html +++ b/v2_adminpanel/templates/dashboard.html @@ -68,10 +68,10 @@ @@ -274,7 +274,14 @@
@@ -284,25 +291,46 @@
- + + +
-
{{ type|upper }}
+
+ {{ type|upper }} +
- {{ data.available }} / {{ data.total }} verfügbar + {{ data.available }} / {{ data.total }} verfügbar {{ data.available_percent }}%
-
+
+ style="width: {{ data.available_percent }}%" + data-bs-toggle="tooltip" + title="{{ data.available }} von {{ data.total }} verfügbar">
+
+
+ + + {{ data.allocated }} zugeteilt + + + {% if data.quarantine > 0 %} + + + {{ data.quarantine }} in Quarantäne + + + {% endif %}
- - {{ data.allocated }} zugeteilt | {{ data.quarantine }} in Quarantäne -
@@ -318,9 +346,15 @@ {% endif %}
{% if resource_warning %} - diff --git a/v2_adminpanel/templates/index.html b/v2_adminpanel/templates/index.html index 1f80db5..1fe95cd 100644 --- a/v2_adminpanel/templates/index.html +++ b/v2_adminpanel/templates/index.html @@ -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 = `${available}`; 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 += ` `; + + // 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 = ` 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 += ` Kritisch`; + } else if (available < 50) { + element.classList.remove('text-success', 'text-danger'); + element.classList.add('text-warning'); + statusHtml += ` Niedrig`; + } else { + element.classList.remove('text-danger', 'text-warning'); + element.classList.add('text-success'); + statusHtml += ` OK`; + } } + + 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 = ` +
+
+ +
+
+ `; + 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 diff --git a/v2_adminpanel/templates/resources.html b/v2_adminpanel/templates/resources.html index 4e2c878..7e01499 100644 --- a/v2_adminpanel/templates/resources.html +++ b/v2_adminpanel/templates/resources.html @@ -175,6 +175,11 @@

Verwalten Sie Domains, IPs und Telefonnummern

+ {% if request.referrer and 'customers-licenses' in request.referrer %} + + Zurück zu Kunden + + {% endif %} ➕ Ressourcen hinzufügen @@ -184,6 +189,9 @@ 📄 Report + + Dashboard +
@@ -366,12 +374,17 @@ {% if resource[5] %}
- {{ resource[5] }}
-
{{ resource[6] }}
+ {% else %} - {% endif %}