Initial commit
Dieser Commit ist enthalten in:
514
v2_adminpanel/templates/batch_form.html
Normale Datei
514
v2_adminpanel/templates/batch_form.html
Normale Datei
@ -0,0 +1,514 @@
|
||||
{% 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 %}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren