UI Verbesserung (Bei Log war Button)
Dieser Commit ist enthalten in:
@@ -29,6 +29,7 @@ from io import BytesIO
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -2797,6 +2798,130 @@ def export_licenses():
|
|||||||
download_name=f'{filename}.xlsx'
|
download_name=f'{filename}.xlsx'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route("/export/audit")
|
||||||
|
@login_required
|
||||||
|
def export_audit():
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Holen der Filter-Parameter
|
||||||
|
filter_user = request.args.get('user', '')
|
||||||
|
filter_action = request.args.get('action', '')
|
||||||
|
filter_entity = request.args.get('entity', '')
|
||||||
|
export_format = request.args.get('format', 'excel')
|
||||||
|
|
||||||
|
# SQL Query mit Filtern
|
||||||
|
query = """
|
||||||
|
SELECT id, timestamp, username, action, entity_type, entity_id,
|
||||||
|
old_values, new_values, ip_address, user_agent, additional_info
|
||||||
|
FROM audit_log
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if filter_user:
|
||||||
|
query += " AND username ILIKE %s"
|
||||||
|
params.append(f'%{filter_user}%')
|
||||||
|
|
||||||
|
if filter_action:
|
||||||
|
query += " AND action = %s"
|
||||||
|
params.append(filter_action)
|
||||||
|
|
||||||
|
if filter_entity:
|
||||||
|
query += " AND entity_type = %s"
|
||||||
|
params.append(filter_entity)
|
||||||
|
|
||||||
|
query += " ORDER BY timestamp DESC"
|
||||||
|
|
||||||
|
cur.execute(query, params)
|
||||||
|
audit_logs = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Daten für Export vorbereiten
|
||||||
|
data = []
|
||||||
|
for log in audit_logs:
|
||||||
|
action_text = {
|
||||||
|
'CREATE': 'Erstellt',
|
||||||
|
'UPDATE': 'Bearbeitet',
|
||||||
|
'DELETE': 'Gelöscht',
|
||||||
|
'LOGIN': 'Anmeldung',
|
||||||
|
'LOGOUT': 'Abmeldung',
|
||||||
|
'AUTO_LOGOUT': 'Auto-Logout',
|
||||||
|
'EXPORT': 'Export',
|
||||||
|
'GENERATE_KEY': 'Key generiert',
|
||||||
|
'CREATE_BATCH': 'Batch erstellt',
|
||||||
|
'BACKUP': 'Backup erstellt',
|
||||||
|
'LOGIN_2FA_SUCCESS': '2FA-Anmeldung',
|
||||||
|
'LOGIN_2FA_BACKUP': '2FA-Backup-Code',
|
||||||
|
'LOGIN_2FA_FAILED': '2FA-Fehlgeschlagen',
|
||||||
|
'LOGIN_BLOCKED': 'Login-Blockiert',
|
||||||
|
'RESTORE': 'Wiederhergestellt',
|
||||||
|
'PASSWORD_CHANGE': 'Passwort geändert',
|
||||||
|
'2FA_ENABLED': '2FA aktiviert',
|
||||||
|
'2FA_DISABLED': '2FA deaktiviert'
|
||||||
|
}.get(log[3], log[3])
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
'ID': log[0],
|
||||||
|
'Zeitstempel': log[1].strftime('%d.%m.%Y %H:%M:%S'),
|
||||||
|
'Benutzer': log[2],
|
||||||
|
'Aktion': action_text,
|
||||||
|
'Entität': log[4],
|
||||||
|
'Entität-ID': log[5] or '',
|
||||||
|
'IP-Adresse': log[8] or '',
|
||||||
|
'Zusatzinfo': log[10] or ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# DataFrame erstellen
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
# Timestamp für Dateiname
|
||||||
|
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
|
||||||
|
filename = f'audit_log_export_{timestamp}'
|
||||||
|
|
||||||
|
# Audit Log für Export
|
||||||
|
log_audit('EXPORT', 'audit_log',
|
||||||
|
additional_info=f"{export_format.upper()} Export mit {len(data)} Einträgen")
|
||||||
|
|
||||||
|
if export_format == 'csv':
|
||||||
|
# CSV Export
|
||||||
|
output = io.StringIO()
|
||||||
|
# UTF-8 BOM für Excel
|
||||||
|
output.write('\ufeff')
|
||||||
|
df.to_csv(output, index=False, sep=';', encoding='utf-8')
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(output.getvalue().encode('utf-8')),
|
||||||
|
mimetype='text/csv;charset=utf-8',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.csv'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Excel Export
|
||||||
|
output = BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, index=False, sheet_name='Audit Log')
|
||||||
|
|
||||||
|
# Spaltenbreiten anpassen
|
||||||
|
worksheet = writer.sheets['Audit Log']
|
||||||
|
for idx, col in enumerate(df.columns):
|
||||||
|
max_length = max(
|
||||||
|
df[col].astype(str).map(len).max(),
|
||||||
|
len(col)
|
||||||
|
) + 2
|
||||||
|
worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.xlsx'
|
||||||
|
)
|
||||||
|
|
||||||
@app.route("/export/customers")
|
@app.route("/export/customers")
|
||||||
@login_required
|
@login_required
|
||||||
def export_customers():
|
def export_customers():
|
||||||
@@ -2878,6 +3003,273 @@ def export_customers():
|
|||||||
download_name=f'{filename}.xlsx'
|
download_name=f'{filename}.xlsx'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route("/export/sessions")
|
||||||
|
@login_required
|
||||||
|
def export_sessions():
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Holen des Session-Typs (active oder ended)
|
||||||
|
session_type = request.args.get('type', 'active')
|
||||||
|
export_format = request.args.get('format', 'excel')
|
||||||
|
|
||||||
|
# Daten je nach Typ abrufen
|
||||||
|
if session_type == 'active':
|
||||||
|
# Aktive Lizenz-Sessions
|
||||||
|
cur.execute("""
|
||||||
|
SELECT s.id, l.license_key, c.name as customer_name, s.session_id,
|
||||||
|
s.started_at, s.last_heartbeat,
|
||||||
|
EXTRACT(EPOCH FROM (NOW() - s.started_at))::INT as duration_seconds,
|
||||||
|
s.ip_address, s.user_agent
|
||||||
|
FROM sessions s
|
||||||
|
JOIN licenses l ON s.license_id = l.id
|
||||||
|
JOIN customers c ON l.customer_id = c.id
|
||||||
|
WHERE s.is_active = true
|
||||||
|
ORDER BY s.last_heartbeat DESC
|
||||||
|
""")
|
||||||
|
sessions = cur.fetchall()
|
||||||
|
|
||||||
|
# Daten für Export vorbereiten
|
||||||
|
data = []
|
||||||
|
for sess in sessions:
|
||||||
|
duration = sess[6]
|
||||||
|
hours = duration // 3600
|
||||||
|
minutes = (duration % 3600) // 60
|
||||||
|
seconds = duration % 60
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
'Session-ID': sess[0],
|
||||||
|
'Lizenzschlüssel': sess[1],
|
||||||
|
'Kunde': sess[2],
|
||||||
|
'Session-ID (Tech)': sess[3],
|
||||||
|
'Startzeit': sess[4].strftime('%d.%m.%Y %H:%M:%S'),
|
||||||
|
'Letzte Aktivität': sess[5].strftime('%d.%m.%Y %H:%M:%S'),
|
||||||
|
'Dauer': f"{hours}h {minutes}m {seconds}s",
|
||||||
|
'IP-Adresse': sess[7],
|
||||||
|
'Browser': sess[8]
|
||||||
|
})
|
||||||
|
|
||||||
|
sheet_name = 'Aktive Sessions'
|
||||||
|
filename_prefix = 'aktive_sessions'
|
||||||
|
else:
|
||||||
|
# Beendete Lizenz-Sessions
|
||||||
|
cur.execute("""
|
||||||
|
SELECT s.id, l.license_key, c.name as customer_name, s.session_id,
|
||||||
|
s.started_at, s.ended_at,
|
||||||
|
EXTRACT(EPOCH FROM (s.ended_at - s.started_at))::INT as duration_seconds,
|
||||||
|
s.ip_address, s.user_agent
|
||||||
|
FROM sessions s
|
||||||
|
JOIN licenses l ON s.license_id = l.id
|
||||||
|
JOIN customers c ON l.customer_id = c.id
|
||||||
|
WHERE s.is_active = false AND s.ended_at IS NOT NULL
|
||||||
|
ORDER BY s.ended_at DESC
|
||||||
|
LIMIT 1000
|
||||||
|
""")
|
||||||
|
sessions = cur.fetchall()
|
||||||
|
|
||||||
|
# Daten für Export vorbereiten
|
||||||
|
data = []
|
||||||
|
for sess in sessions:
|
||||||
|
duration = sess[6] if sess[6] else 0
|
||||||
|
hours = duration // 3600
|
||||||
|
minutes = (duration % 3600) // 60
|
||||||
|
seconds = duration % 60
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
'Session-ID': sess[0],
|
||||||
|
'Lizenzschlüssel': sess[1],
|
||||||
|
'Kunde': sess[2],
|
||||||
|
'Session-ID (Tech)': sess[3],
|
||||||
|
'Startzeit': sess[4].strftime('%d.%m.%Y %H:%M:%S'),
|
||||||
|
'Endzeit': sess[5].strftime('%d.%m.%Y %H:%M:%S') if sess[5] else '',
|
||||||
|
'Dauer': f"{hours}h {minutes}m {seconds}s",
|
||||||
|
'IP-Adresse': sess[7],
|
||||||
|
'Browser': sess[8]
|
||||||
|
})
|
||||||
|
|
||||||
|
sheet_name = 'Beendete Sessions'
|
||||||
|
filename_prefix = 'beendete_sessions'
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# DataFrame erstellen
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
# Timestamp für Dateiname
|
||||||
|
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
|
||||||
|
filename = f'{filename_prefix}_export_{timestamp}'
|
||||||
|
|
||||||
|
# Audit Log für Export
|
||||||
|
log_audit('EXPORT', 'sessions',
|
||||||
|
additional_info=f"{export_format.upper()} Export von {session_type} Sessions mit {len(data)} Einträgen")
|
||||||
|
|
||||||
|
if export_format == 'csv':
|
||||||
|
# CSV Export
|
||||||
|
output = io.StringIO()
|
||||||
|
# UTF-8 BOM für Excel
|
||||||
|
output.write('\ufeff')
|
||||||
|
df.to_csv(output, index=False, sep=';', encoding='utf-8')
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(output.getvalue().encode('utf-8')),
|
||||||
|
mimetype='text/csv;charset=utf-8',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.csv'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Excel Export
|
||||||
|
output = BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, index=False, sheet_name=sheet_name)
|
||||||
|
|
||||||
|
# Spaltenbreiten anpassen
|
||||||
|
worksheet = writer.sheets[sheet_name]
|
||||||
|
for idx, col in enumerate(df.columns):
|
||||||
|
max_length = max(
|
||||||
|
df[col].astype(str).map(len).max(),
|
||||||
|
len(col)
|
||||||
|
) + 2
|
||||||
|
worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.xlsx'
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/export/resources")
|
||||||
|
@login_required
|
||||||
|
def export_resources():
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Holen der Filter-Parameter
|
||||||
|
filter_type = request.args.get('type', '')
|
||||||
|
filter_status = request.args.get('status', '')
|
||||||
|
search_query = request.args.get('search', '')
|
||||||
|
show_test = request.args.get('show_test', 'false').lower() == 'true'
|
||||||
|
export_format = request.args.get('format', 'excel')
|
||||||
|
|
||||||
|
# SQL Query mit Filtern
|
||||||
|
query = """
|
||||||
|
SELECT r.id, r.type, r.value, r.status, r.license_id, r.created_at, r.allocated_at,
|
||||||
|
l.license_key, c.name as customer_name, c.email as customer_email,
|
||||||
|
l.type as license_type
|
||||||
|
FROM resource_pools r
|
||||||
|
LEFT JOIN licenses l ON r.license_id = l.id
|
||||||
|
LEFT JOIN customers c ON l.customer_id = c.id
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
# Filter für Testdaten
|
||||||
|
if not show_test:
|
||||||
|
query += " AND (l.is_test = false OR l.is_test IS NULL)"
|
||||||
|
|
||||||
|
# Filter für Ressourcentyp
|
||||||
|
if filter_type:
|
||||||
|
query += " AND r.type = %s"
|
||||||
|
params.append(filter_type)
|
||||||
|
|
||||||
|
# Filter für Status
|
||||||
|
if filter_status:
|
||||||
|
query += " AND r.status = %s"
|
||||||
|
params.append(filter_status)
|
||||||
|
|
||||||
|
# Suchfilter
|
||||||
|
if search_query:
|
||||||
|
query += " AND (r.value ILIKE %s OR l.license_key ILIKE %s OR c.name ILIKE %s)"
|
||||||
|
params.extend([f'%{search_query}%', f'%{search_query}%', f'%{search_query}%'])
|
||||||
|
|
||||||
|
query += " ORDER BY r.id DESC"
|
||||||
|
|
||||||
|
cur.execute(query, params)
|
||||||
|
resources = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Daten für Export vorbereiten
|
||||||
|
data = []
|
||||||
|
for res in resources:
|
||||||
|
status_text = {
|
||||||
|
'available': 'Verfügbar',
|
||||||
|
'allocated': 'Zugewiesen',
|
||||||
|
'quarantine': 'Quarantäne'
|
||||||
|
}.get(res[3], res[3])
|
||||||
|
|
||||||
|
type_text = {
|
||||||
|
'domain': 'Domain',
|
||||||
|
'ipv4': 'IPv4',
|
||||||
|
'phone': 'Telefon'
|
||||||
|
}.get(res[1], res[1])
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
'ID': res[0],
|
||||||
|
'Typ': type_text,
|
||||||
|
'Ressource': res[2],
|
||||||
|
'Status': status_text,
|
||||||
|
'Lizenzschlüssel': res[7] or '',
|
||||||
|
'Kunde': res[8] or '',
|
||||||
|
'Kunden-Email': res[9] or '',
|
||||||
|
'Lizenztyp': res[10] or '',
|
||||||
|
'Erstellt am': res[5].strftime('%d.%m.%Y %H:%M:%S') if res[5] else '',
|
||||||
|
'Zugewiesen am': res[6].strftime('%d.%m.%Y %H:%M:%S') if res[6] else ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# DataFrame erstellen
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
# Timestamp für Dateiname
|
||||||
|
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
|
||||||
|
filename = f'resources_export_{timestamp}'
|
||||||
|
|
||||||
|
# Audit Log für Export
|
||||||
|
log_audit('EXPORT', 'resources',
|
||||||
|
additional_info=f"{export_format.upper()} Export mit {len(data)} Ressourcen")
|
||||||
|
|
||||||
|
if export_format == 'csv':
|
||||||
|
# CSV Export
|
||||||
|
output = io.StringIO()
|
||||||
|
# UTF-8 BOM für Excel
|
||||||
|
output.write('\ufeff')
|
||||||
|
df.to_csv(output, index=False, sep=';', encoding='utf-8')
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(output.getvalue().encode('utf-8')),
|
||||||
|
mimetype='text/csv;charset=utf-8',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.csv'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Excel Export
|
||||||
|
output = BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, index=False, sheet_name='Resources')
|
||||||
|
|
||||||
|
# Spaltenbreiten anpassen
|
||||||
|
worksheet = writer.sheets['Resources']
|
||||||
|
for idx, col in enumerate(df.columns):
|
||||||
|
max_length = max(
|
||||||
|
df[col].astype(str).map(len).max(),
|
||||||
|
len(col)
|
||||||
|
) + 2
|
||||||
|
worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.xlsx'
|
||||||
|
)
|
||||||
|
|
||||||
@app.route("/audit")
|
@app.route("/audit")
|
||||||
@login_required
|
@login_required
|
||||||
def audit_log():
|
def audit_log():
|
||||||
|
|||||||
@@ -63,14 +63,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="mb-4">
|
||||||
<h2>📝 Log</h2>
|
<h2>📝 Log</h2>
|
||||||
<div>
|
|
||||||
<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>
|
|
||||||
<a href="/backups" class="btn btn-secondary">💾 Backups</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter -->
|
<!-- Filter -->
|
||||||
@@ -119,7 +113,20 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<a href="/audit" class="btn btn-outline-secondary">Zurücksetzen</a>
|
<div class="d-flex gap-2">
|
||||||
|
<a href="/audit" class="btn btn-outline-secondary">Zurücksetzen</a>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-download"></i> Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/export/audit?format=excel&user={{ filter_user }}&action={{ filter_action }}&entity={{ filter_entity }}">
|
||||||
|
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/audit?format=csv&user={{ filter_user }}&action={{ filter_action }}&entity={{ filter_entity }}">
|
||||||
|
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -444,24 +444,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="sidebar-header mt-4">
|
|
||||||
<i class="bi bi-download"></i> Export
|
|
||||||
</div>
|
|
||||||
<ul class="sidebar-nav">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/export/licenses?format=excel">
|
|
||||||
<i class="bi bi-file-earmark-excel"></i>
|
|
||||||
<span>Excel Export</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/export/licenses?format=csv">
|
|
||||||
<i class="bi bi-file-earmark-text"></i>
|
|
||||||
<span>CSV Export</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
|
|||||||
@@ -6,7 +6,25 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<h2 class="mb-4">Kunden & Lizenzen</h2>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0">Kunden & Lizenzen</h2>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-download"></i> Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/export/customers?format=excel{% if request.args.get('show_test') %}&include_test=true{% endif %}">
|
||||||
|
<i class="bi bi-file-earmark-excel text-success"></i> Kunden (Excel)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/customers?format=csv{% if request.args.get('show_test') %}&include_test=true{% endif %}">
|
||||||
|
<i class="bi bi-file-earmark-text"></i> Kunden (CSV)</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=excel{% if request.args.get('show_test') %}&include_test=true{% endif %}{% if selected_customer_id %}&customer_id={{ selected_customer_id }}{% endif %}">
|
||||||
|
<i class="bi bi-file-earmark-excel text-success"></i> Lizenzen (Excel)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=csv{% if request.args.get('show_test') %}&include_test=true{% endif %}{% if selected_customer_id %}&customer_id={{ selected_customer_id }}{% endif %}">
|
||||||
|
<i class="bi bi-file-earmark-text"></i> Lizenzen (CSV)</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Kundenliste (Links) -->
|
<!-- Kundenliste (Links) -->
|
||||||
|
|||||||
@@ -290,7 +290,20 @@
|
|||||||
<div class="card-header bg-white">
|
<div class="card-header bg-white">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">📋 Ressourcen-Liste</h5>
|
<h5 class="mb-0">📋 Ressourcen-Liste</h5>
|
||||||
<span class="badge bg-secondary">{{ total }} Einträge</span>
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-download"></i> Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/export/resources?format=excel&type={{ filter_type }}&status={{ filter_status }}&search={{ search_query }}&show_test={{ show_test }}">
|
||||||
|
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/resources?format=csv&type={{ filter_type }}&status={{ filter_status }}&search={{ search_query }}&show_test={{ show_test }}">
|
||||||
|
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-secondary">{{ total }} Einträge</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|||||||
@@ -56,7 +56,23 @@
|
|||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2>Session-Tracking</h2>
|
<h2>Session-Tracking</h2>
|
||||||
<div>
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-download"></i> Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><h6 class="dropdown-header">Aktive Sessions</h6></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/sessions?type=active&format=excel">
|
||||||
|
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/sessions?type=active&format=csv">
|
||||||
|
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><h6 class="dropdown-header">Beendete Sessions</h6></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/sessions?type=ended&format=excel">
|
||||||
|
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/sessions?type=ended&format=csv">
|
||||||
|
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren