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
|
||||
- ✅ Audit-Log-Integration
|
||||
- ✅ 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
|
||||
# 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'))
|
||||
|
||||
@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")
|
||||
@login_required
|
||||
def licenses():
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
.action-AUTO_LOGOUT { color: #fd7e14; }
|
||||
.action-EXPORT { color: #ffc107; }
|
||||
.action-GENERATE_KEY { color: #20c997; }
|
||||
.action-CREATE_BATCH { color: #6610f2; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,6 +66,7 @@
|
||||
<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="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>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@@ -116,6 +118,7 @@
|
||||
{% elif log[3] == 'AUTO_LOGOUT' %}⏰ Auto-Logout
|
||||
{% elif log[3] == 'EXPORT' %}📥 Export
|
||||
{% elif log[3] == 'GENERATE_KEY' %}🔑 Key generiert
|
||||
{% elif log[3] == 'CREATE_BATCH' %}🔑 Batch erstellt
|
||||
{% else %}{{ log[3] }}
|
||||
{% endif %}
|
||||
</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>
|
||||
<div>
|
||||
<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="/customers" class="btn btn-secondary">👥 Kunden</a>
|
||||
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<h2>Neue Lizenz erstellen</h2>
|
||||
<div>
|
||||
<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="/customers" class="btn btn-secondary">👥 Kunden</a>
|
||||
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren