diff --git a/JOURNAL.md b/JOURNAL.md index d9d2d6e..59a5731 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -303,4 +303,30 @@ Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker - Responsive Tabellen mit Bootstrap **Hinweis:** -Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden. \ No newline at end of file +Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden. + +### 2025-01-06 - Export-Funktion implementiert +- CSV und Excel Export für Lizenzen und Kunden +- Formatierte Ausgabe mit deutschen Datumsformaten +- UTF-8 Unterstützung für Sonderzeichen + +**Neue Features:** +- **Lizenz-Export**: Alle Lizenzen mit Kundeninformationen +- **Kunden-Export**: Alle Kunden mit Lizenzstatistiken +- **Format-Optionen**: Excel (.xlsx) und CSV (.csv) +- **Deutsche Formatierung**: Datum als dd.mm.yyyy, Status auf Deutsch +- **UTF-8 Export**: Korrekte Kodierung für Umlaute +- **Export-Buttons**: Dropdown-Menüs in Lizenz- und Kundenübersicht + +**Geänderte Dateien:** +- v2_adminpanel/app.py (export_licenses() und export_customers() Routen) +- v2_adminpanel/requirements.txt (pandas und openpyxl hinzugefügt) +- v2_adminpanel/templates/licenses.html (Export-Dropdown hinzugefügt) +- v2_adminpanel/templates/customers.html (Export-Dropdown hinzugefügt) + +**Technische Details:** +- Pandas für Datenverarbeitung +- OpenPyXL für Excel-Export +- CSV mit Semikolon-Trennung für deutsche Excel-Kompatibilität +- Automatische Spaltenbreite in Excel +- BOM für UTF-8 CSV (Excel-Kompatibilität) \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 8b3c4aa..58d8016 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -1,9 +1,12 @@ import os import psycopg2 -from flask import Flask, render_template, request, redirect, session, url_for +from flask import Flask, render_template, request, redirect, session, url_for, send_file from flask_session import Session from functools import wraps from dotenv import load_dotenv +import pandas as pd +from datetime import datetime +import io load_dotenv() @@ -544,5 +547,166 @@ def end_session(session_id): return redirect("/sessions") +@app.route("/export/licenses") +@login_required +def export_licenses(): + conn = get_connection() + cur = conn.cursor() + + # Alle Lizenzen mit Kundeninformationen abrufen + cur.execute(""" + SELECT l.id, l.license_key, c.name as customer_name, c.email as customer_email, + l.license_type, l.valid_from, l.valid_until, l.is_active, + CASE + WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen' + WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'Läuft bald ab' + ELSE 'Aktiv' + END as status + FROM licenses l + JOIN customers c ON l.customer_id = c.id + ORDER BY l.id + """) + + # Spaltennamen + columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', + 'Gültig von', 'Gültig bis', 'Aktiv', 'Status'] + + # Daten in DataFrame + data = cur.fetchall() + df = pd.DataFrame(data, columns=columns) + + # Datumsformatierung + df['Gültig von'] = pd.to_datetime(df['Gültig von']).dt.strftime('%d.%m.%Y') + df['Gültig bis'] = pd.to_datetime(df['Gültig bis']).dt.strftime('%d.%m.%Y') + + # Typ und Aktiv Status anpassen + df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'}) + df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'}) + + cur.close() + conn.close() + + # Export Format + export_format = request.args.get('format', 'excel') + filename = f'lizenzen_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}' + + if export_format == 'csv': + # CSV Export + output = io.StringIO() + df.to_csv(output, index=False, encoding='utf-8-sig', sep=';') + output.seek(0) + + return send_file( + io.BytesIO(output.getvalue().encode('utf-8-sig')), + mimetype='text/csv', + as_attachment=True, + download_name=f'{filename}.csv' + ) + else: + # Excel Export + output = io.BytesIO() + with pd.ExcelWriter(output, engine='openpyxl') as writer: + df.to_excel(writer, sheet_name='Lizenzen', index=False) + + # Formatierung + worksheet = writer.sheets['Lizenzen'] + for column in worksheet.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = min(max_length + 2, 50) + worksheet.column_dimensions[column_letter].width = adjusted_width + + 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") +@login_required +def export_customers(): + conn = get_connection() + cur = conn.cursor() + + # Alle Kunden mit Lizenzstatistiken + cur.execute(""" + SELECT c.id, c.name, c.email, c.created_at, + COUNT(l.id) as total_licenses, + COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses, + COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses + FROM customers c + LEFT JOIN licenses l ON c.id = l.customer_id + GROUP BY c.id, c.name, c.email, c.created_at + ORDER BY c.id + """) + + # Spaltennamen + columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', + 'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen'] + + # Daten in DataFrame + data = cur.fetchall() + df = pd.DataFrame(data, columns=columns) + + # Datumsformatierung + df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M') + + cur.close() + conn.close() + + # Export Format + export_format = request.args.get('format', 'excel') + filename = f'kunden_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}' + + if export_format == 'csv': + # CSV Export + output = io.StringIO() + df.to_csv(output, index=False, encoding='utf-8-sig', sep=';') + output.seek(0) + + return send_file( + io.BytesIO(output.getvalue().encode('utf-8-sig')), + mimetype='text/csv', + as_attachment=True, + download_name=f'{filename}.csv' + ) + else: + # Excel Export + output = io.BytesIO() + with pd.ExcelWriter(output, engine='openpyxl') as writer: + df.to_excel(writer, sheet_name='Kunden', index=False) + + # Formatierung + worksheet = writer.sheets['Kunden'] + for column in worksheet.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = min(max_length + 2, 50) + worksheet.column_dimensions[column_letter].width = adjusted_width + + output.seek(0) + + return send_file( + output, + mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + as_attachment=True, + download_name=f'{filename}.xlsx' + ) + if __name__ == "__main__": app.run(host="0.0.0.0", port=443, ssl_context='adhoc') diff --git a/v2_adminpanel/requirements.txt b/v2_adminpanel/requirements.txt index 9720db6..51bf61b 100644 --- a/v2_adminpanel/requirements.txt +++ b/v2_adminpanel/requirements.txt @@ -3,3 +3,5 @@ flask-session psycopg2-binary python-dotenv pyopenssl +pandas +openpyxl diff --git a/v2_adminpanel/templates/customers.html b/v2_adminpanel/templates/customers.html index 815b613..d5cd092 100644 --- a/v2_adminpanel/templates/customers.html +++ b/v2_adminpanel/templates/customers.html @@ -24,6 +24,15 @@ ➕ Neue Lizenz 📋 Lizenzen 🟢 Sessions +