514 Zeilen
22 KiB
HTML
514 Zeilen
22 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Batch-Lizenzen erstellen{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container py-5">
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h2>🔑 Batch-Lizenzen erstellen</h2>
|
||
<a href="{{ url_for('customers.customers_licenses') }}" class="btn btn-secondary">← Zurück zur Übersicht</a>
|
||
</div>
|
||
|
||
<div class="alert alert-info">
|
||
<strong>ℹ️ Batch-Generierung:</strong> Erstellen Sie mehrere Lizenzen auf einmal für einen Kunden.
|
||
Die Lizenzen werden automatisch generiert und können anschließend als CSV exportiert werden.
|
||
</div>
|
||
|
||
<!-- Flash Messages -->
|
||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||
{% if messages %}
|
||
{% for category, message in messages %}
|
||
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
|
||
{{ message }}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
<!-- Customer Type Indicator -->
|
||
<div id="customerTypeIndicator" class="alert d-none mb-3" role="alert">
|
||
<i class="fas fa-info-circle"></i> <span id="customerTypeMessage"></span>
|
||
</div>
|
||
|
||
<form method="post" action="{{ url_for('batch.batch_create') }}" accept-charset="UTF-8">
|
||
<div class="row g-3">
|
||
<div class="col-md-12">
|
||
<label for="customerSelect" class="form-label">Kunde auswählen</label>
|
||
<select class="form-select" id="customerSelect" name="customer_id" required>
|
||
<option value="">🔍 Kunde suchen oder neuen Kunden anlegen...</option>
|
||
<option value="new">➕ Neuer Kunde</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6" id="customerNameDiv" style="display: none;">
|
||
<label for="customerName" class="form-label">Kundenname</label>
|
||
<input type="text" class="form-control" id="customerName" name="customer_name"
|
||
placeholder="Firma GmbH" accept-charset="UTF-8">
|
||
</div>
|
||
<div class="col-md-6" id="emailDiv" style="display: none;">
|
||
<label for="email" class="form-label">E-Mail</label>
|
||
<input type="email" class="form-control" id="email" name="email"
|
||
placeholder="kontakt@firma.de" accept-charset="UTF-8">
|
||
</div>
|
||
|
||
<div class="col-md-3">
|
||
<label for="quantity" class="form-label">Anzahl Lizenzen</label>
|
||
<input type="number" class="form-control" id="quantity" name="quantity"
|
||
min="1" max="100" value="10" required>
|
||
<div class="form-text">Max. 100 Lizenzen pro Batch</div>
|
||
</div>
|
||
|
||
<div class="col-md-3">
|
||
<label for="licenseType" class="form-label">Lizenztyp</label>
|
||
<select class="form-select" id="licenseType" name="license_type" required>
|
||
<option value="full">Vollversion</option>
|
||
<option value="test">Testversion</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="col-md-2">
|
||
<label for="validFrom" class="form-label">Gültig ab</label>
|
||
<input type="date" class="form-control" id="validFrom" name="valid_from" required>
|
||
</div>
|
||
|
||
<div class="col-md-1">
|
||
<label for="duration" class="form-label">Laufzeit</label>
|
||
<input type="number" class="form-control" id="duration" name="duration" value="1" min="1" required>
|
||
</div>
|
||
|
||
<div class="col-md-2">
|
||
<label for="durationType" class="form-label">Einheit</label>
|
||
<select class="form-select" id="durationType" name="duration_type" required>
|
||
<option value="days">Tage</option>
|
||
<option value="months">Monate</option>
|
||
<option value="years" selected>Jahre</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="col-md-2">
|
||
<label for="validUntil" class="form-label">Gültig bis</label>
|
||
<input type="date" class="form-control" id="validUntil" name="valid_until" readonly style="background-color: #e9ecef;">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Resource Pool Allocation -->
|
||
<div class="card mt-4">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">
|
||
<i class="fas fa-server"></i> Ressourcen-Zuweisung pro Lizenz
|
||
<small class="text-muted float-end" id="resourceStatus"></small>
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<label for="domainCount" class="form-label">
|
||
<i class="fas fa-globe"></i> Domains
|
||
</label>
|
||
<select class="form-select" id="domainCount" name="domain_count" required>
|
||
{% for i in range(11) %}
|
||
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<small class="form-text text-muted">
|
||
Verfügbar: <span id="domainsAvailable" class="fw-bold">-</span>
|
||
| Benötigt: <span id="domainsNeeded" class="fw-bold">-</span>
|
||
</small>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label for="ipv4Count" class="form-label">
|
||
<i class="fas fa-network-wired"></i> IPv4-Adressen
|
||
</label>
|
||
<select class="form-select" id="ipv4Count" name="ipv4_count" required>
|
||
{% for i in range(11) %}
|
||
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<small class="form-text text-muted">
|
||
Verfügbar: <span id="ipv4Available" class="fw-bold">-</span>
|
||
| Benötigt: <span id="ipv4Needed" class="fw-bold">-</span>
|
||
</small>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label for="phoneCount" class="form-label">
|
||
<i class="fas fa-phone"></i> Telefonnummern
|
||
</label>
|
||
<select class="form-select" id="phoneCount" name="phone_count" required>
|
||
{% for i in range(11) %}
|
||
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<small class="form-text text-muted">
|
||
Verfügbar: <span id="phoneAvailable" class="fw-bold">-</span>
|
||
| Benötigt: <span id="phoneNeeded" class="fw-bold">-</span>
|
||
</small>
|
||
</div>
|
||
</div>
|
||
<div class="alert alert-warning mt-3 mb-0" role="alert">
|
||
<i class="fas fa-exclamation-triangle"></i>
|
||
<strong>Batch-Ressourcen:</strong> Jede Lizenz erhält die angegebene Anzahl an Ressourcen.
|
||
Bei 10 Lizenzen mit je 2 Domains werden insgesamt 20 Domains benötigt.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Device Limit -->
|
||
<div class="card mt-4">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">
|
||
<i class="fas fa-laptop"></i> Gerätelimit pro Lizenz
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<label for="deviceLimit" class="form-label">
|
||
Maximale Anzahl Geräte pro Lizenz
|
||
</label>
|
||
<select class="form-select" id="deviceLimit" name="device_limit" required>
|
||
{% for i in range(1, 11) %}
|
||
<option value="{{ i }}" {% if i == 3 %}selected{% endif %}>{{ i }} {% if i == 1 %}Gerät{% else %}Geräte{% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<small class="form-text text-muted">
|
||
Jede generierte Lizenz kann auf maximal dieser Anzahl von Geräten gleichzeitig aktiviert werden.
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="mt-4 d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary btn-lg">
|
||
🔑 Batch generieren
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary" onclick="previewKeys()">
|
||
👁️ Vorschau
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Vorschau Modal -->
|
||
<div class="modal fade" id="previewModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Vorschau der generierten Keys</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Es werden <strong id="previewQuantity">10</strong> Lizenzen im folgenden Format generiert:</p>
|
||
<div class="bg-light p-3 rounded font-monospace" id="previewFormat">
|
||
AF-F-YYYYMM-XXXX-YYYY-ZZZZ
|
||
</div>
|
||
<p class="mt-3 mb-0">Die Keys werden automatisch eindeutig generiert und in der Datenbank gespeichert.</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Funktion zur Berechnung des Ablaufdatums
|
||
function calculateValidUntil() {
|
||
const validFrom = document.getElementById('validFrom').value;
|
||
const duration = parseInt(document.getElementById('duration').value) || 1;
|
||
const durationType = document.getElementById('durationType').value;
|
||
|
||
if (!validFrom) return;
|
||
|
||
const startDate = new Date(validFrom);
|
||
let endDate = new Date(startDate);
|
||
|
||
switch(durationType) {
|
||
case 'days':
|
||
endDate.setDate(endDate.getDate() + duration);
|
||
break;
|
||
case 'months':
|
||
endDate.setMonth(endDate.getMonth() + duration);
|
||
break;
|
||
case 'years':
|
||
endDate.setFullYear(endDate.getFullYear() + duration);
|
||
break;
|
||
}
|
||
|
||
// Kein Tag abziehen - die Lizenz ist bis einschließlich des Enddatums gültig
|
||
|
||
document.getElementById('validUntil').value = endDate.toISOString().split('T')[0];
|
||
}
|
||
|
||
// Event Listener für Änderungen
|
||
document.getElementById('validFrom').addEventListener('change', calculateValidUntil);
|
||
document.getElementById('duration').addEventListener('input', calculateValidUntil);
|
||
document.getElementById('durationType').addEventListener('change', calculateValidUntil);
|
||
|
||
// Setze heutiges Datum als Standard
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
document.getElementById('validFrom').value = today;
|
||
|
||
// Initialize customer is_fake map
|
||
window.customerIsFakeMap = {};
|
||
|
||
// Berechne initiales Ablaufdatum
|
||
calculateValidUntil();
|
||
|
||
// Initialisiere Select2 für Kundenauswahl
|
||
$('#customerSelect').select2({
|
||
theme: 'bootstrap-5',
|
||
placeholder: '🔍 Kunde suchen oder neuen Kunden anlegen...',
|
||
allowClear: true,
|
||
ajax: {
|
||
url: '/api/customers',
|
||
dataType: 'json',
|
||
delay: 250,
|
||
data: function (params) {
|
||
return {
|
||
q: params.term,
|
||
page: params.page || 1
|
||
};
|
||
},
|
||
processResults: function (data, params) {
|
||
params.page = params.page || 1;
|
||
|
||
// Store is_fake status for each customer
|
||
const results = data.results || [];
|
||
results.forEach(customer => {
|
||
if (customer.id !== 'new') {
|
||
window.customerIsFakeMap[customer.id] = customer.is_fake || false;
|
||
}
|
||
});
|
||
|
||
// "Neuer Kunde" Option immer oben anzeigen
|
||
if (params.page === 1) {
|
||
results.unshift({
|
||
id: 'new',
|
||
text: '➕ Neuer Kunde',
|
||
isNew: true
|
||
});
|
||
}
|
||
|
||
return {
|
||
results: results,
|
||
pagination: data.pagination
|
||
};
|
||
},
|
||
cache: true
|
||
},
|
||
minimumInputLength: 0,
|
||
language: {
|
||
inputTooShort: function() { return ''; },
|
||
noResults: function() { return 'Keine Kunden gefunden'; },
|
||
searching: function() { return 'Suche...'; },
|
||
loadingMore: function() { return 'Lade weitere Ergebnisse...'; }
|
||
}
|
||
});
|
||
|
||
// Event Handler für Kundenauswahl
|
||
$('#customerSelect').on('select2:select', function (e) {
|
||
const selectedValue = e.params.data.id;
|
||
const nameDiv = document.getElementById('customerNameDiv');
|
||
const emailDiv = document.getElementById('emailDiv');
|
||
const nameInput = document.getElementById('customerName');
|
||
const emailInput = document.getElementById('email');
|
||
|
||
if (selectedValue === 'new') {
|
||
// Zeige Eingabefelder für neuen Kunden
|
||
nameDiv.style.display = 'block';
|
||
emailDiv.style.display = 'block';
|
||
nameInput.required = true;
|
||
emailInput.required = true;
|
||
|
||
// Zeige Indikator für neuen Kunden
|
||
showCustomerTypeIndicator('new');
|
||
} else {
|
||
// Verstecke Eingabefelder bei bestehendem Kunden
|
||
nameDiv.style.display = 'none';
|
||
emailDiv.style.display = 'none';
|
||
nameInput.required = false;
|
||
emailInput.required = false;
|
||
nameInput.value = '';
|
||
emailInput.value = '';
|
||
|
||
// Zeige Indikator basierend auf Kundendaten
|
||
if (e.params.data.is_fake !== undefined) {
|
||
showCustomerTypeIndicator(e.params.data.is_fake ? 'fake' : 'real');
|
||
}
|
||
}
|
||
|
||
// Update resource availability check when customer changes
|
||
checkResourceAvailability();
|
||
});
|
||
|
||
// Clear handler
|
||
$('#customerSelect').on('select2:clear', function (e) {
|
||
document.getElementById('customerNameDiv').style.display = 'none';
|
||
document.getElementById('emailDiv').style.display = 'none';
|
||
document.getElementById('customerName').required = false;
|
||
document.getElementById('email').required = false;
|
||
hideCustomerTypeIndicator();
|
||
window.customerIsFakeMap = {};
|
||
checkResourceAvailability();
|
||
});
|
||
|
||
// Resource Availability Check
|
||
checkResourceAvailability();
|
||
|
||
// Event Listener für Resource Count und Quantity Änderungen
|
||
document.getElementById('domainCount').addEventListener('change', checkResourceAvailability);
|
||
document.getElementById('ipv4Count').addEventListener('change', checkResourceAvailability);
|
||
document.getElementById('phoneCount').addEventListener('change', checkResourceAvailability);
|
||
document.getElementById('quantity').addEventListener('input', checkResourceAvailability);
|
||
});
|
||
|
||
// Vorschau-Funktion
|
||
function previewKeys() {
|
||
const quantity = document.getElementById('quantity').value;
|
||
const type = document.getElementById('licenseType').value;
|
||
const typeChar = type === 'full' ? 'F' : 'T';
|
||
const date = new Date();
|
||
const dateStr = date.getFullYear() + ('0' + (date.getMonth() + 1)).slice(-2);
|
||
|
||
document.getElementById('previewQuantity').textContent = quantity;
|
||
document.getElementById('previewFormat').textContent = `AF-${typeChar}-${dateStr}-XXXX-YYYY-ZZZZ`;
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// Validierung
|
||
document.getElementById('quantity').addEventListener('input', function(e) {
|
||
if (e.target.value > 100) {
|
||
e.target.value = 100;
|
||
}
|
||
if (e.target.value < 1) {
|
||
e.target.value = 1;
|
||
}
|
||
});
|
||
|
||
// Funktion zur Prüfung der Ressourcen-Verfügbarkeit für Batch
|
||
function checkResourceAvailability() {
|
||
const quantity = parseInt(document.getElementById('quantity').value) || 1;
|
||
const domainCount = parseInt(document.getElementById('domainCount').value) || 0;
|
||
const ipv4Count = parseInt(document.getElementById('ipv4Count').value) || 0;
|
||
const phoneCount = parseInt(document.getElementById('phoneCount').value) || 0;
|
||
|
||
// Berechne Gesamtbedarf
|
||
const totalDomains = domainCount * quantity;
|
||
const totalIpv4 = ipv4Count * quantity;
|
||
const totalPhones = phoneCount * quantity;
|
||
|
||
// Update "Benötigt" Anzeigen
|
||
document.getElementById('domainsNeeded').textContent = totalDomains;
|
||
document.getElementById('ipv4Needed').textContent = totalIpv4;
|
||
document.getElementById('phoneNeeded').textContent = totalPhones;
|
||
|
||
// Get customer's is_fake status
|
||
const selectedCustomer = document.getElementById('customer_id');
|
||
let isFake = 'false';
|
||
if (selectedCustomer && selectedCustomer.value && window.customerIsFakeMap) {
|
||
isFake = window.customerIsFakeMap[selectedCustomer.value] ? 'true' : 'false';
|
||
}
|
||
|
||
// API-Call zur Verfügbarkeitsprüfung
|
||
fetch(`/api/resources/check-availability?domain=${totalDomains}&ipv4=${totalIpv4}&phone=${totalPhones}&is_fake=${isFake}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// Update der Verfügbarkeitsanzeigen
|
||
updateAvailabilityDisplay('domainsAvailable', data.domain_available, totalDomains);
|
||
updateAvailabilityDisplay('ipv4Available', data.ipv4_available, totalIpv4);
|
||
updateAvailabilityDisplay('phoneAvailable', data.phone_available, totalPhones);
|
||
|
||
// Gesamtstatus aktualisieren
|
||
updateBatchResourceStatus(data, totalDomains, totalIpv4, totalPhones, quantity);
|
||
})
|
||
.catch(error => {
|
||
console.error('Fehler bei Verfügbarkeitsprüfung:', error);
|
||
});
|
||
}
|
||
|
||
// Hilfsfunktion zur Anzeige der Verfügbarkeit
|
||
function updateAvailabilityDisplay(elementId, available, requested) {
|
||
const element = document.getElementById(elementId);
|
||
element.textContent = available;
|
||
|
||
const neededElement = element.parentElement.querySelector('.fw-bold:last-child');
|
||
|
||
if (requested > 0 && available < requested) {
|
||
element.classList.remove('text-success');
|
||
element.classList.add('text-danger');
|
||
neededElement.classList.add('text-danger');
|
||
neededElement.classList.remove('text-success');
|
||
} else if (available < 50) {
|
||
element.classList.remove('text-success', 'text-danger');
|
||
element.classList.add('text-warning');
|
||
} else {
|
||
element.classList.remove('text-danger', 'text-warning');
|
||
element.classList.add('text-success');
|
||
neededElement.classList.remove('text-danger');
|
||
neededElement.classList.add('text-success');
|
||
}
|
||
}
|
||
|
||
// Gesamtstatus der Ressourcen-Verfügbarkeit für Batch
|
||
function updateBatchResourceStatus(data, totalDomains, totalIpv4, totalPhones, quantity) {
|
||
const statusElement = document.getElementById('resourceStatus');
|
||
let hasIssue = false;
|
||
let message = '';
|
||
|
||
if (totalDomains > 0 && data.domain_available < totalDomains) {
|
||
hasIssue = true;
|
||
message = `⚠️ Nicht genügend Domains (${data.domain_available}/${totalDomains})`;
|
||
} else if (totalIpv4 > 0 && data.ipv4_available < totalIpv4) {
|
||
hasIssue = true;
|
||
message = `⚠️ Nicht genügend IPv4-Adressen (${data.ipv4_available}/${totalIpv4})`;
|
||
} else if (totalPhones > 0 && data.phone_available < totalPhones) {
|
||
hasIssue = true;
|
||
message = `⚠️ Nicht genügend Telefonnummern (${data.phone_available}/${totalPhones})`;
|
||
} else {
|
||
message = `✅ Ressourcen für ${quantity} Lizenzen verfügbar`;
|
||
}
|
||
|
||
statusElement.textContent = message;
|
||
statusElement.className = hasIssue ? 'text-danger' : 'text-success';
|
||
|
||
// Disable submit button if not enough resources
|
||
const submitButton = document.querySelector('button[type="submit"]');
|
||
submitButton.disabled = hasIssue;
|
||
if (hasIssue) {
|
||
submitButton.classList.add('btn-secondary');
|
||
submitButton.classList.remove('btn-primary');
|
||
} else {
|
||
submitButton.classList.add('btn-primary');
|
||
submitButton.classList.remove('btn-secondary');
|
||
}
|
||
}
|
||
|
||
// Funktion zur Anzeige des Kundentyp-Indikators
|
||
function showCustomerTypeIndicator(type) {
|
||
const indicator = document.getElementById('customerTypeIndicator');
|
||
const message = document.getElementById('customerTypeMessage');
|
||
|
||
indicator.classList.remove('d-none', 'alert-info', 'alert-warning', 'alert-success');
|
||
|
||
if (type === 'new') {
|
||
indicator.classList.add('alert-info');
|
||
message.textContent = 'Neue Kunden werden in der Testphase als TEST-Kunden erstellt. Alle Batch-Lizenzen werden automatisch als TEST-Lizenzen markiert.';
|
||
} else if (type === 'fake') {
|
||
indicator.classList.add('alert-warning');
|
||
message.textContent = 'Dies ist ein TEST-Kunde. Alle Batch-Lizenzen werden automatisch als TEST-Lizenzen markiert und von der Software ignoriert.';
|
||
} else if (type === 'real') {
|
||
indicator.classList.add('alert-success');
|
||
message.textContent = 'Dies ist ein PRODUKTIV-Kunde. Alle Batch-Lizenzen werden als produktive Lizenzen erstellt.';
|
||
}
|
||
}
|
||
|
||
function hideCustomerTypeIndicator() {
|
||
document.getElementById('customerTypeIndicator').classList.add('d-none');
|
||
}
|
||
</script>
|
||
{% endblock %} |