Batch-Lizenzen
Dieser Commit ist enthalten in:
65
JOURNAL.md
65
JOURNAL.md
@@ -964,3 +964,68 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
|
|||||||
- ✅ Validierung und Fehlerbehandlung
|
- ✅ Validierung und Fehlerbehandlung
|
||||||
- ✅ Audit-Log-Integration
|
- ✅ Audit-Log-Integration
|
||||||
- ✅ Form-Action-Bug behoben
|
- ✅ Form-Action-Bug behoben
|
||||||
|
|
||||||
|
### 2025-06-07 - Batch-Lizenzgenerierung implementiert
|
||||||
|
- Mehrere Lizenzen auf einmal für einen Kunden erstellen
|
||||||
|
|
||||||
|
**Implementierte Features:**
|
||||||
|
|
||||||
|
1. **Batch-Formular (/batch):**
|
||||||
|
- Kunde und E-Mail eingeben
|
||||||
|
- Anzahl der Lizenzen (1-100)
|
||||||
|
- Lizenztyp (Vollversion/Testversion)
|
||||||
|
- Gültigkeitszeitraum für alle Lizenzen
|
||||||
|
- Vorschau-Modal zeigt Key-Format
|
||||||
|
- Standard-Datum-Einstellungen (heute + 1 Jahr)
|
||||||
|
|
||||||
|
2. **Backend-Verarbeitung:**
|
||||||
|
- Route `/batch` für GET (Formular) und POST (Generierung)
|
||||||
|
- Generiert die angegebene Anzahl eindeutiger Keys
|
||||||
|
- Speichert alle in einer Transaktion
|
||||||
|
- Kunde wird automatisch angelegt (falls nicht vorhanden)
|
||||||
|
- ON CONFLICT für existierende Kunden
|
||||||
|
- Audit-Log-Eintrag mit CREATE_BATCH Aktion
|
||||||
|
|
||||||
|
3. **Ergebnis-Seite:**
|
||||||
|
- Zeigt alle generierten Lizenzen in Tabellenform
|
||||||
|
- Kundeninformationen und Gültigkeitszeitraum
|
||||||
|
- Einzelne Keys können kopiert werden (📋 Button)
|
||||||
|
- Alle Keys auf einmal kopieren
|
||||||
|
- Druckfunktion für physische Ausgabe
|
||||||
|
- Link zur Lizenzübersicht mit Kundenfilter
|
||||||
|
|
||||||
|
4. **Export-Funktionalität:**
|
||||||
|
- Route `/batch/export` für CSV-Download
|
||||||
|
- Speichert Batch-Daten in Session für Export
|
||||||
|
- CSV mit UTF-8 BOM für Excel-Kompatibilität
|
||||||
|
- Enthält Kundeninfo, Generierungsdatum und alle Keys
|
||||||
|
- Format: Nr;Lizenzschlüssel;Typ
|
||||||
|
- Dateiname: batch_licenses_KUNDE_TIMESTAMP.csv
|
||||||
|
|
||||||
|
5. **Integration:**
|
||||||
|
- Batch-Button in Navigation (Dashboard, Einzellizenz-Seite)
|
||||||
|
- CREATE_BATCH Aktion im Audit-Log (Farbe: #6610f2)
|
||||||
|
- Session-basierte Export-Daten
|
||||||
|
- Flash-Messages für Feedback
|
||||||
|
|
||||||
|
**Sicherheit:**
|
||||||
|
- Limit von 100 Lizenzen pro Batch
|
||||||
|
- Login-Required für alle Routen
|
||||||
|
- Transaktionale Datenbank-Operationen
|
||||||
|
- Validierung der Eingaben
|
||||||
|
|
||||||
|
**Beispiel-Workflow:**
|
||||||
|
1. Admin geht zu `/batch`
|
||||||
|
2. Gibt Kunde "Firma GmbH", Anzahl "25", Typ "Vollversion" ein
|
||||||
|
3. System generiert 25 eindeutige Keys
|
||||||
|
4. Ergebnis-Seite zeigt alle Keys
|
||||||
|
5. Admin kann CSV exportieren oder Keys kopieren
|
||||||
|
6. Kunde erhält die Lizenzen
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- ✅ Batch-Formular vollständig implementiert
|
||||||
|
- ✅ Backend-Generierung mit Transaktionen
|
||||||
|
- ✅ Export als CSV
|
||||||
|
- ✅ Copy-to-Clipboard Funktionalität
|
||||||
|
- ✅ Audit-Log-Integration
|
||||||
|
- ✅ Navigation aktualisiert
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
# https://curl.se/docs/http-cookies.html
|
# https://curl.se/docs/http-cookies.html
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
#HttpOnly_localhost FALSE / FALSE 1749329677 admin_session DJ5-gm8DCBYcqZyLqo7pYzvq-FoFBRAYkvWPn37aAo4
|
#HttpOnly_localhost FALSE / FALSE 1749330711 admin_session kL1GAl-qrgTLBwfFpeQLngAfFne2ehrnKbWE3M3awqE
|
||||||
|
|||||||
@@ -981,6 +981,140 @@ def create_license():
|
|||||||
|
|
||||||
return render_template("index.html", username=session.get('username'))
|
return render_template("index.html", username=session.get('username'))
|
||||||
|
|
||||||
|
@app.route("/batch", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def batch_licenses():
|
||||||
|
"""Batch-Generierung mehrerer Lizenzen für einen Kunden"""
|
||||||
|
if request.method == "POST":
|
||||||
|
# Formulardaten
|
||||||
|
name = request.form["customer_name"]
|
||||||
|
email = request.form["email"]
|
||||||
|
license_type = request.form["license_type"]
|
||||||
|
quantity = int(request.form["quantity"])
|
||||||
|
valid_from = request.form["valid_from"]
|
||||||
|
valid_until = request.form["valid_until"]
|
||||||
|
|
||||||
|
# Sicherheitslimit
|
||||||
|
if quantity < 1 or quantity > 100:
|
||||||
|
flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
|
||||||
|
return redirect(url_for('batch_licenses'))
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Kunde einfügen (falls nicht vorhanden)
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO customers (name, email, created_at)
|
||||||
|
VALUES (%s, %s, NOW())
|
||||||
|
ON CONFLICT (name, email) DO UPDATE SET name=EXCLUDED.name
|
||||||
|
RETURNING id
|
||||||
|
""", (name, email))
|
||||||
|
customer_id = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# Lizenzen generieren und speichern
|
||||||
|
generated_licenses = []
|
||||||
|
for i in range(quantity):
|
||||||
|
# Eindeutigen Key generieren
|
||||||
|
attempts = 0
|
||||||
|
while attempts < 10:
|
||||||
|
license_key = generate_license_key(license_type)
|
||||||
|
cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (license_key,))
|
||||||
|
if not cur.fetchone():
|
||||||
|
break
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
|
# Lizenz einfügen
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO licenses (license_key, customer_id, license_type,
|
||||||
|
valid_from, valid_until, is_active, created_at)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, true, NOW())
|
||||||
|
RETURNING id
|
||||||
|
""", (license_key, customer_id, license_type, valid_from, valid_until))
|
||||||
|
license_id = cur.fetchone()[0]
|
||||||
|
|
||||||
|
generated_licenses.append({
|
||||||
|
'id': license_id,
|
||||||
|
'key': license_key,
|
||||||
|
'type': license_type
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Audit-Log
|
||||||
|
log_audit('CREATE_BATCH', 'license',
|
||||||
|
new_values={'customer': name, 'quantity': quantity, 'type': license_type},
|
||||||
|
additional_info=f"Batch-Generierung von {quantity} Lizenzen")
|
||||||
|
|
||||||
|
# Session für Export speichern
|
||||||
|
session['batch_export'] = {
|
||||||
|
'customer': name,
|
||||||
|
'email': email,
|
||||||
|
'licenses': generated_licenses,
|
||||||
|
'valid_from': valid_from,
|
||||||
|
'valid_until': valid_until,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
flash(f'{quantity} Lizenzen erfolgreich generiert!', 'success')
|
||||||
|
return render_template("batch_result.html",
|
||||||
|
customer=name,
|
||||||
|
email=email,
|
||||||
|
licenses=generated_licenses,
|
||||||
|
valid_from=valid_from,
|
||||||
|
valid_until=valid_until)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logging.error(f"Fehler bei Batch-Generierung: {str(e)}")
|
||||||
|
flash('Fehler bei der Batch-Generierung!', 'error')
|
||||||
|
return redirect(url_for('batch_licenses'))
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# GET Request
|
||||||
|
return render_template("batch_form.html")
|
||||||
|
|
||||||
|
@app.route("/batch/export")
|
||||||
|
@login_required
|
||||||
|
def export_batch():
|
||||||
|
"""Exportiert die zuletzt generierten Batch-Lizenzen"""
|
||||||
|
batch_data = session.get('batch_export')
|
||||||
|
if not batch_data:
|
||||||
|
flash('Keine Batch-Daten zum Exportieren vorhanden!', 'error')
|
||||||
|
return redirect(url_for('batch_licenses'))
|
||||||
|
|
||||||
|
# CSV generieren
|
||||||
|
output = io.StringIO()
|
||||||
|
output.write('\ufeff') # UTF-8 BOM für Excel
|
||||||
|
|
||||||
|
# Header
|
||||||
|
output.write(f"Kunde: {batch_data['customer']}\n")
|
||||||
|
output.write(f"E-Mail: {batch_data['email']}\n")
|
||||||
|
output.write(f"Generiert am: {datetime.fromisoformat(batch_data['timestamp']).strftime('%d.%m.%Y %H:%M')}\n")
|
||||||
|
output.write(f"Gültig von: {batch_data['valid_from']} bis {batch_data['valid_until']}\n")
|
||||||
|
output.write("\n")
|
||||||
|
output.write("Nr;Lizenzschlüssel;Typ\n")
|
||||||
|
|
||||||
|
# Lizenzen
|
||||||
|
for i, license in enumerate(batch_data['licenses'], 1):
|
||||||
|
typ_text = "Vollversion" if license['type'] == 'full' else "Testversion"
|
||||||
|
output.write(f"{i};{license['key']};{typ_text}\n")
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
# Audit-Log
|
||||||
|
log_audit('EXPORT', 'batch_licenses',
|
||||||
|
additional_info=f"Export von {len(batch_data['licenses'])} Batch-Lizenzen")
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(output.getvalue().encode('utf-8-sig')),
|
||||||
|
mimetype='text/csv',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||||
|
)
|
||||||
|
|
||||||
@app.route("/licenses")
|
@app.route("/licenses")
|
||||||
@login_required
|
@login_required
|
||||||
def licenses():
|
def licenses():
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
.action-AUTO_LOGOUT { color: #fd7e14; }
|
.action-AUTO_LOGOUT { color: #fd7e14; }
|
||||||
.action-EXPORT { color: #ffc107; }
|
.action-EXPORT { color: #ffc107; }
|
||||||
.action-GENERATE_KEY { color: #20c997; }
|
.action-GENERATE_KEY { color: #20c997; }
|
||||||
|
.action-CREATE_BATCH { color: #6610f2; }
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
<option value="AUTO_LOGOUT" {% if filter_action == 'AUTO_LOGOUT' %}selected{% endif %}>⏰ Auto-Logout</option>
|
<option value="AUTO_LOGOUT" {% if filter_action == 'AUTO_LOGOUT' %}selected{% endif %}>⏰ Auto-Logout</option>
|
||||||
<option value="EXPORT" {% if filter_action == 'EXPORT' %}selected{% endif %}>📥 Export</option>
|
<option value="EXPORT" {% if filter_action == 'EXPORT' %}selected{% endif %}>📥 Export</option>
|
||||||
<option value="GENERATE_KEY" {% if filter_action == 'GENERATE_KEY' %}selected{% endif %}>🔑 Key generiert</option>
|
<option value="GENERATE_KEY" {% if filter_action == 'GENERATE_KEY' %}selected{% endif %}>🔑 Key generiert</option>
|
||||||
|
<option value="CREATE_BATCH" {% if filter_action == 'CREATE_BATCH' %}selected{% endif %}>🔑 Batch erstellt</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@@ -116,6 +118,7 @@
|
|||||||
{% elif log[3] == 'AUTO_LOGOUT' %}⏰ Auto-Logout
|
{% elif log[3] == 'AUTO_LOGOUT' %}⏰ Auto-Logout
|
||||||
{% elif log[3] == 'EXPORT' %}📥 Export
|
{% elif log[3] == 'EXPORT' %}📥 Export
|
||||||
{% elif log[3] == 'GENERATE_KEY' %}🔑 Key generiert
|
{% elif log[3] == 'GENERATE_KEY' %}🔑 Key generiert
|
||||||
|
{% elif log[3] == 'CREATE_BATCH' %}🔑 Batch erstellt
|
||||||
{% else %}{{ log[3] }}
|
{% else %}{{ log[3] }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
143
v2_adminpanel/templates/batch_form.html
Normale Datei
143
v2_adminpanel/templates/batch_form.html
Normale Datei
@@ -0,0 +1,143 @@
|
|||||||
|
{% 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>
|
||||||
|
<div>
|
||||||
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
|
<a href="/create" class="btn btn-secondary">➕ Einzellizenz</a>
|
||||||
|
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
||||||
|
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
||||||
|
</div>
|
||||||
|
</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 %}
|
||||||
|
|
||||||
|
<form method="post" action="/batch" accept-charset="UTF-8">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<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" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<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-3">
|
||||||
|
<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-3">
|
||||||
|
<label for="validUntil" class="form-label">Gültig bis</label>
|
||||||
|
<input type="date" class="form-control" id="validUntil" name="valid_until" required>
|
||||||
|
</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-YYYYMMFT-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>
|
||||||
|
// Setze heutiges Datum als Standard
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('validFrom').value = today;
|
||||||
|
|
||||||
|
// Setze valid_until auf 1 Jahr später
|
||||||
|
const oneYearLater = new Date();
|
||||||
|
oneYearLater.setFullYear(oneYearLater.getFullYear() + 1);
|
||||||
|
document.getElementById('validUntil').value = oneYearLater.toISOString().split('T')[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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-${dateStr}${typeChar}-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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
157
v2_adminpanel/templates/batch_result.html
Normale Datei
157
v2_adminpanel/templates/batch_result.html
Normale Datei
@@ -0,0 +1,157 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Batch-Lizenzen generiert{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>✅ Batch-Lizenzen erfolgreich generiert</h2>
|
||||||
|
<div>
|
||||||
|
<a href="/batch" class="btn btn-primary">🔑 Weitere Batch erstellen</a>
|
||||||
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h5 class="alert-heading">🎉 {{ licenses|length }} Lizenzen wurden erfolgreich generiert!</h5>
|
||||||
|
<p class="mb-0">Die Lizenzen wurden in der Datenbank gespeichert und dem Kunden zugeordnet.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kunden-Info -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">📋 Kundeninformationen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Kunde:</strong> {{ customer }}</p>
|
||||||
|
<p><strong>E-Mail:</strong> {{ email or 'Nicht angegeben' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Gültig von:</strong> {{ valid_from }}</p>
|
||||||
|
<p><strong>Gültig bis:</strong> {{ valid_until }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export-Optionen -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">📥 Export-Optionen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Exportieren Sie die generierten Lizenzen für den Kunden:</p>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="/batch/export" class="btn btn-success">
|
||||||
|
📄 Als CSV exportieren
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-outline-primary" onclick="copyAllKeys()">
|
||||||
|
📋 Alle Keys kopieren
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="window.print()">
|
||||||
|
🖨️ Drucken
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generierte Lizenzen -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">🔑 Generierte Lizenzen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50">#</th>
|
||||||
|
<th>Lizenzschlüssel</th>
|
||||||
|
<th width="120">Typ</th>
|
||||||
|
<th width="100">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for license in licenses %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ loop.index }}</td>
|
||||||
|
<td class="font-monospace">{{ license.key }}</td>
|
||||||
|
<td>
|
||||||
|
{% if license.type == 'full' %}
|
||||||
|
<span class="badge bg-success">Vollversion</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info">Testversion</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary"
|
||||||
|
onclick="copyKey('{{ license.key }}')"
|
||||||
|
title="Kopieren">
|
||||||
|
📋
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hinweis -->
|
||||||
|
<div class="alert alert-info mt-4">
|
||||||
|
<strong>💡 Tipp:</strong> Die generierten Lizenzen sind sofort aktiv und können verwendet werden.
|
||||||
|
Sie finden alle Lizenzen auch in der <a href="/licenses?search={{ customer }}">Lizenzübersicht</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Textarea für Kopieren (unsichtbar) -->
|
||||||
|
<textarea id="copyArea" style="position: absolute; left: -9999px;"></textarea>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
.btn, .alert-info, .card-header h5 { display: none !important; }
|
||||||
|
.card { border: 1px solid #000 !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Einzelnen Key kopieren
|
||||||
|
function copyKey(key) {
|
||||||
|
navigator.clipboard.writeText(key).then(function() {
|
||||||
|
// Visuelles Feedback
|
||||||
|
event.target.textContent = '✓';
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.textContent = '📋';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Keys kopieren
|
||||||
|
function copyAllKeys() {
|
||||||
|
const keys = [];
|
||||||
|
{% for license in licenses %}
|
||||||
|
keys.push('{{ license.key }}');
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
const text = keys.join('\n');
|
||||||
|
const textarea = document.getElementById('copyArea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
// Visuelles Feedback
|
||||||
|
event.target.textContent = '✓ Kopiert!';
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.textContent = '📋 Alle Keys kopieren';
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback für moderne Browser
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<div>
|
<div>
|
||||||
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
||||||
|
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
||||||
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
||||||
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
||||||
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<h2>Neue Lizenz erstellen</h2>
|
<h2>Neue Lizenz erstellen</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
|
<a href="/batch" class="btn btn-primary">🔑 Batch</a>
|
||||||
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
||||||
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
||||||
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren