Zuweisung über Kunden & Lizenzen geht
Dieser Commit ist enthalten in:
@@ -4469,26 +4469,60 @@ def allocate_resources_api():
|
|||||||
@login_required
|
@login_required
|
||||||
def check_resource_availability():
|
def check_resource_availability():
|
||||||
"""Prüft verfügbare Ressourcen"""
|
"""Prüft verfügbare Ressourcen"""
|
||||||
|
resource_type = request.args.get('type', '')
|
||||||
|
count = request.args.get('count', 10, type=int)
|
||||||
|
show_test = request.args.get('show_test', 'false').lower() == 'true'
|
||||||
|
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
cur.execute("""
|
if resource_type:
|
||||||
SELECT
|
# Spezifische Ressourcen für einen Typ
|
||||||
resource_type,
|
cur.execute("""
|
||||||
COUNT(*) as available
|
SELECT id, resource_value
|
||||||
FROM resource_pools
|
FROM resource_pools
|
||||||
WHERE status = 'available'
|
WHERE status = 'available'
|
||||||
GROUP BY resource_type
|
AND resource_type = %s
|
||||||
""")
|
AND is_test = %s
|
||||||
|
ORDER BY resource_value
|
||||||
availability = {}
|
LIMIT %s
|
||||||
for row in cur.fetchall():
|
""", (resource_type, show_test, count))
|
||||||
availability[row[0]] = row[1]
|
|
||||||
|
resources = []
|
||||||
cur.close()
|
for row in cur.fetchall():
|
||||||
conn.close()
|
resources.append({
|
||||||
|
'id': row[0],
|
||||||
return jsonify(availability)
|
'value': row[1]
|
||||||
|
})
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'available': resources,
|
||||||
|
'type': resource_type,
|
||||||
|
'count': len(resources)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Zusammenfassung aller Typen
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
resource_type,
|
||||||
|
COUNT(*) as available
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE status = 'available'
|
||||||
|
AND is_test = %s
|
||||||
|
GROUP BY resource_type
|
||||||
|
""", (show_test,))
|
||||||
|
|
||||||
|
availability = {}
|
||||||
|
for row in cur.fetchall():
|
||||||
|
availability[row[0]] = row[1]
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(availability)
|
||||||
|
|
||||||
@app.route('/api/global-search', methods=['GET'])
|
@app.route('/api/global-search', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
|
|
||||||
<!-- Modal für Ressourcen-Verwaltung -->
|
<!-- Modal für Ressourcen-Verwaltung -->
|
||||||
<div class="modal fade" id="resourceManagementModal" tabindex="-1">
|
<div class="modal fade" id="resourceManagementModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Ressourcen verwalten</h5>
|
<h5 class="modal-title">Ressourcen verwalten</h5>
|
||||||
@@ -244,8 +244,11 @@
|
|||||||
<!-- Wird dynamisch gefüllt -->
|
<!-- Wird dynamisch gefüllt -->
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||||
<button type="button" class="btn btn-primary" onclick="saveResourceChanges()">
|
<button type="button" class="btn btn-primary" onclick="saveResourceChanges()" id="saveResourcesBtn">
|
||||||
<i class="bi bi-save"></i> Änderungen speichern
|
<i class="bi bi-save"></i> Änderungen speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -310,6 +313,44 @@
|
|||||||
.btn-link {
|
.btn-link {
|
||||||
padding: 0 4px;
|
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>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -672,6 +713,9 @@ function showResourceManagement(licenseId) {
|
|||||||
const license = currentLicenses[licenseId];
|
const license = currentLicenses[licenseId];
|
||||||
if (!license) return;
|
if (!license) return;
|
||||||
|
|
||||||
|
// Speichere License-ID im Modal für späteren Zugriff
|
||||||
|
document.getElementById('resourceManagementModal').dataset.licenseId = licenseId;
|
||||||
|
|
||||||
// Lade aktuelle Ressourcen-Informationen
|
// Lade aktuelle Ressourcen-Informationen
|
||||||
fetch(`/api/license/${licenseId}/resources`)
|
fetch(`/api/license/${licenseId}/resources`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -775,7 +819,62 @@ function showResourceManagement(licenseId) {
|
|||||||
content += `
|
content += `
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
<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>
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
Das Quarantäne-setzen einer Ressource entfernt sie von der Lizenz und markiert sie als problematisch.
|
Das Quarantäne-setzen einer Ressource entfernt sie von der Lizenz und markiert sie als problematisch.
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -783,6 +882,20 @@ function showResourceManagement(licenseId) {
|
|||||||
document.getElementById('resourceManagementContent').innerHTML = content;
|
document.getElementById('resourceManagementContent').innerHTML = content;
|
||||||
const modal = new bootstrap.Modal(document.getElementById('resourceManagementModal'));
|
const modal = new bootstrap.Modal(document.getElementById('resourceManagementModal'));
|
||||||
modal.show();
|
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 => {
|
.catch(error => {
|
||||||
@@ -801,9 +914,187 @@ function quarantineResource(resourceId, resourceValue) {
|
|||||||
alert('Diese Funktion wird noch implementiert');
|
alert('Diese Funktion wird noch implementiert');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy-Funktion für Speichern (wird später implementiert)
|
// Lade verfügbare Ressourcen
|
||||||
|
function loadAvailableResources(licenseId) {
|
||||||
|
// Hole show_test Parameter aus der URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const showTest = urlParams.get('show_test') === 'true';
|
||||||
|
|
||||||
|
// Lade verfügbare Domains
|
||||||
|
fetch(`/api/resources/check-availability?type=domain&count=200&show_test=${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_test=${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_test=${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() {
|
function saveResourceChanges() {
|
||||||
alert('Diese Funktion wird noch implementiert');
|
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('/api/resources/allocate', {
|
||||||
|
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
|
// Geräte-Verwaltung anzeigen
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren