From 97b87465e42398834af56bdfe5a14301fdd96f9a Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Mon, 9 Jun 2025 19:13:44 +0200 Subject: [PATCH] Kunden & Lizenzen - Part1 --- JOURNAL.md | 266 +++++++++ THE_ROAD_SO_FAR.md | 47 +- v2_adminpanel/app.py | 534 +++++++++++------- v2_adminpanel/templates/base.html | 26 + v2_adminpanel/templates/customers.html | 3 + .../templates/customers_licenses.html | 443 +++++++++++++++ .../templates/customers_licenses_old.html | 491 ++++++++++++++++ v2_adminpanel/templates/dashboard.html | 5 +- v2_adminpanel/templates/edit_customer.html | 9 +- v2_adminpanel/templates/edit_license.html | 7 +- v2_adminpanel/templates/index.html | 18 + v2_adminpanel/templates/licenses.html | 3 + 12 files changed, 1625 insertions(+), 227 deletions(-) create mode 100644 v2_adminpanel/templates/customers_licenses.html create mode 100644 v2_adminpanel/templates/customers_licenses_old.html diff --git a/JOURNAL.md b/JOURNAL.md index 0ce55b7..58738bd 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -2188,6 +2188,88 @@ docker-compose up -d **Status:** ✅ Alle Lizenzschlüssel erfolgreich migriert +### 2025-06-09: Kombinierte Kunden-Lizenz-Ansicht implementiert + +**Problem:** +- Umständliche Navigation zwischen Kunden- und Lizenzseiten +- Viel Hin-und-Her-Springen bei der Verwaltung +- Kontext-Verlust beim Wechseln zwischen Ansichten + +**Lösung:** +Master-Detail View mit 2-Spalten Layout implementiert + +**Phase 1-3 abgeschlossen:** +1. **Backend-Implementierung:** + - Neue Route `/customers-licenses` für kombinierte Ansicht + - API-Endpoints für AJAX: `/api/customer//licenses`, `/api/customer//quick-stats` + - API-Endpoint `/api/license//quick-edit` für Inline-Bearbeitung + - Optimierte SQL-Queries mit JOIN für Performance + +2. **Template-Erstellung:** + - Neues Template `customers_licenses.html` mit Master-Detail Layout + - Links: Kundenliste (30%) mit Suchfeld + - Rechts: Lizenzen des ausgewählten Kunden (70%) + - Responsive Design (Mobile: untereinander) + - JavaScript für dynamisches Laden ohne Seitenreload + - Keyboard-Navigation (↑↓ für Kundenwechsel) + +3. **Integration:** + - Dashboard: Neuer Button "Kunden & Lizenzen" + - Customers-Seite: Link zur kombinierten Ansicht + - Licenses-Seite: Link zur kombinierten Ansicht + - Lizenz-Erstellung: Unterstützung für vorausgewählten Kunden + - API /api/customers erweitert für Einzelabruf per ID + +**Features:** +- Live-Suche in Kundenliste +- Quick-Actions: Copy License Key, Toggle Status +- Modal für neue Lizenz direkt aus Kundenansicht +- URL-Update ohne Reload für Bookmarking +- Loading-States während AJAX-Calls +- Visuelles Feedback (aktiver Kunde hervorgehoben) + +**Noch ausstehend:** +- Phase 4: Inline-Edit für Lizenzdetails +- Phase 5: Erweiterte Error-Handling und Polish + +**Geänderte Dateien:** +- `v2_adminpanel/app.py` - Neue Routen und API-Endpoints +- `v2_adminpanel/templates/customers_licenses.html` - Neues Template +- `v2_adminpanel/templates/dashboard.html` - Neuer Button +- `v2_adminpanel/templates/customers.html` - Link zur kombinierten Ansicht +- `v2_adminpanel/templates/licenses.html` - Link zur kombinierten Ansicht +- `v2_adminpanel/templates/index.html` - Unterstützung für preselected_customer_id + +**Status:** ✅ Grundfunktionalität implementiert und funktionsfähig + +### 2025-06-09: Kombinierte Ansicht - Fertigstellung und TODOs aktualisiert + +**Abgeschlossen:** +- Phase 1-3 der kombinierten Kunden-Lizenz-Ansicht vollständig implementiert +- Master-Detail Layout funktioniert einwandfrei +- AJAX-basiertes Laden ohne Seitenreload +- Keyboard-Navigation mit Pfeiltasten +- Quick-Actions für Copy und Toggle Status +- Integration in alle relevanten Seiten + +**THE_ROAD_SO_FAR.md aktualisiert:** +- Kombinierte Ansicht als "Erledigt" markiert +- Von "In Arbeit" zu "Abgeschlossen" verschoben +- Status dokumentiert + +**Verbesserung gegenüber vorher:** +- Kein Hin-und-Her-Springen mehr zwischen Seiten +- Kontext bleibt erhalten beim Arbeiten mit Kunden +- Schnellere Navigation und bessere Übersicht +- Deutlich verbesserte User Experience + +**Optional für später (Phase 4-5):** +- Inline-Edit für weitere Felder +- Erweiterte Quick-Actions +- Session-basierte Filter-Persistenz + +Die Hauptproblematik der umständlichen Navigation ist damit gelöst! + ### 2025-06-09: Test-Flag für Lizenzen implementiert **Ziel:** @@ -2277,4 +2359,188 @@ UPDATE resource_pools SET is_test = TRUE; -- 20 Ressourcen - Test-Ressourcen werden nur Test-Lizenzen zugewiesen - Alle bestehenden Daten sind jetzt als Test markiert +**Status:** ✅ Vollständig implementiert + +### 2025-06-09 (17:20 - 18:13): Kunden-Lizenz-Verwaltung konsolidiert + +**Problem:** +- Kombinierte Ansicht `/customers-licenses` hatte Formatierungs- und Funktionsprobleme +- Kunden wurden nicht angezeigt +- Bootstrap Icons fehlten +- JavaScript-Fehler beim Modal +- Inkonsistentes Design im Vergleich zu anderen Seiten +- Testkunden-Filter wurde beim Navigieren nicht beibehalten + +**Durchgeführte Änderungen:** + +1. **Frontend-Fixes (base.html):** + - Bootstrap Icons CSS hinzugefügt: `bootstrap-icons@1.11.3/font/bootstrap-icons.min.css` + - Bootstrap JavaScript Bundle bereits vorhanden, Reihenfolge optimiert + +2. **customers_licenses.html komplett überarbeitet:** + - Container-Klasse von `container-fluid` auf `container py-5` geändert + - Emojis und Button-Styling vereinheitlicht (👥 Kunden & Lizenzen) + - Export-Dropdown wie in anderen Ansichten implementiert + - Card-Styling mit Schatten für einheitliches Design + - Checkbox "Testkunden anzeigen" mit Status-Beibehaltung + - JavaScript-Funktionen korrigiert: + - copyToClipboard mit event.currentTarget + - showNewLicenseModal mit Bootstrap Modal + - Header-Update beim AJAX-Kundenwechsel + - URL-Parameter `show_test` wird überall beibehalten + +3. **Backend-Anpassungen (app.py):** + - customers_licenses Route: Optional Testkunden anzeigen mit `show_test` Parameter + - Redirects von `/customers` und `/licenses` auf `/customers-licenses` implementiert + - Alte Route-Funktionen entfernt (kein toter Code mehr) + - edit_license und edit_customer: Redirects behalten show_test Parameter bei + - Dashboard-Links zeigen jetzt auf kombinierte Ansicht + +4. **Navigation optimiert:** + - Dashboard: Klick auf Kunden/Lizenzen-Statistik führt zur kombinierten Ansicht + - Alle Edit-Links behalten den show_test Parameter bei + - Konsistente User Experience beim Navigieren + +**Technische Details:** +- AJAX-Loading für dynamisches Laden der Lizenzen +- Keyboard-Navigation (↑↓) für Kundenliste +- Responsive Design mit Bootstrap Grid +- Modal-Dialoge für Bestätigungen +- Live-Suche in der Kundenliste + +**Resultat:** +- ✅ Einheitliches Design mit anderen Admin-Panel-Seiten +- ✅ Alle Funktionen arbeiten korrekt +- ✅ Testkunden-Filter bleibt erhalten +- ✅ Keine redundanten Views mehr +- ✅ Zentrale Verwaltung für Kunden und Lizenzen + +**Status:** ✅ Vollständig implementiert + +### 2025-06-09: Test-Daten Checkbox Persistenz implementiert + +**Problem:** +- Die "Testkunden anzeigen" Checkbox in `/customers-licenses` verlor ihren Status beim Navigieren zwischen Seiten +- Wenn Benutzer zu anderen Seiten (Resources, Audit Log, etc.) wechselten und zurückkehrten, war die Checkbox wieder deaktiviert +- Benutzer mussten die Checkbox jedes Mal neu aktivieren, was umständlich war + +**Lösung:** +- Globale JavaScript-Funktion `preserveShowTestParameter()` in base.html implementiert +- Die Funktion prüft beim Laden jeder Seite, ob `show_test=true` in der URL ist +- Wenn ja, wird dieser Parameter automatisch an alle internen Links angehängt +- Backend-Route `/create` wurde angepasst, um den Parameter bei Redirects beizubehalten + +**Technische Details:** +1. **base.html** - JavaScript-Funktion hinzugefügt: + - Läuft beim `DOMContentLoaded` Event + - Findet alle Links die mit "/" beginnen + - Fügt `show_test=true` Parameter hinzu wenn nicht bereits vorhanden + - Überspringt Fragment-Links (#) und Links die bereits den Parameter haben + +2. **app.py** - Route-Anpassung: + - `/create` Route behält jetzt `show_test` Parameter bei Redirects bei + - Andere Routen (edit_license, edit_customer) behalten Parameter bereits bei + +**Vorteile:** +- ✅ Konsistente User Experience beim Navigieren +- ✅ Keine manuelle Anpassung aller Links nötig +- ✅ Funktioniert automatisch für alle zukünftigen Links +- ✅ Minimaler Code-Overhead + +**Geänderte Dateien:** +- `v2_adminpanel/templates/base.html` +- `v2_adminpanel/app.py` + +**Status:** ✅ Vollständig implementiert + +### 2025-06-09: Bearbeiten-Button Fehler behoben + +**Problem:** +- Der "Bearbeiten" Button neben dem Kundennamen in der `/customers-licenses` Ansicht verursachte einen Internal Server Error +- Die URL-Konstruktion war fehlerhaft wenn kein `show_test` Parameter vorhanden war +- Die edit_customer.html Template hatte falsche Array-Indizes und veraltete Links + +**Ursache:** +1. Die href-Attribute wurden falsch konstruiert: + - Alt: `/customer/edit/ID{% if show_test %}?ref=customers-licenses&show_test=true{% endif %}` + - Problem: Ohne show_test fehlte das `?ref=customers-licenses` komplett + +2. Die SQL-Abfrage in edit_customer() holte nur 4 Felder, aber das Template erwartete 5: + - Query: `SELECT id, name, email, is_test` + - Template erwartete: `customer[3]` = created_at und `customer[4]` = is_test + +3. Veraltete Links zu `/customers` statt `/customers-licenses` + +**Lösung:** +1. URL-Konstruktion korrigiert in beiden Fällen: + - Neu: `/customer/edit/ID?ref=customers-licenses{% if show_test %}&show_test=true{% endif %}` + +2. SQL-Query erweitert um created_at: + - Neu: `SELECT id, name, email, created_at, is_test` + +3. Template-Indizes korrigiert: + - is_test Checkbox nutzt jetzt `customer[4]` + +4. Navigation-Links aktualisiert: + - Alle Links zeigen jetzt auf `/customers-licenses` mit show_test Parameter + +**Geänderte Dateien:** +- `v2_adminpanel/templates/customers_licenses.html` (Zeilen 103 und 295) +- `v2_adminpanel/app.py` (edit_customer Route) +- `v2_adminpanel/templates/edit_customer.html` + +**Status:** ✅ Behoben + +### 2025-06-09: Unnötigen Lizenz-Erstellungs-Popup entfernt + +**Änderung:** +- Der Bestätigungs-Popup "Möchten Sie eine neue Lizenz für KUNDENNAME erstellen?" wurde entfernt +- Klick auf "Neue Lizenz" Button führt jetzt direkt zur Lizenzerstellung + +**Technische Details:** +- Modal-HTML komplett entfernt +- `showNewLicenseModal()` Funktion vereinfacht - navigiert jetzt direkt zu `/create?customer_id=X` +- URL-Parameter (wie `show_test`) werden dabei beibehalten + +**Vorteile:** +- ✅ Ein Klick weniger für Benutzer +- ✅ Schnellerer Workflow +- ✅ Weniger Code zu warten + +**Geänderte Dateien:** +- `v2_adminpanel/templates/customers_licenses.html` + +**Status:** ✅ Implementiert + +### 2025-06-09: Testkunden-Checkbox bleibt jetzt bei Lizenz/Kunden-Bearbeitung erhalten + +**Problem:** +- Bei "Lizenz bearbeiten" ging der "Testkunden anzeigen" Haken verloren beim Zurückkehren +- Die Navigation-Links in edit_license.html zeigten auf `/licenses` statt `/customers-licenses` +- Der show_test Parameter wurde nur über den unsicheren Referrer übertragen + +**Lösung:** +1. **Navigation-Links korrigiert**: + - Alle Links zeigen jetzt auf `/customers-licenses` mit show_test Parameter + - Betrifft: "Zurück zur Übersicht" und "Abbrechen" Buttons + +2. **Hidden Form Field hinzugefügt**: + - Sowohl in edit_license.html als auch edit_customer.html + - Überträgt den show_test Parameter sicher beim POST + +3. **Route-Logik verbessert**: + - Parameter wird aus Form-Daten ODER GET-Parametern gelesen + - Nicht mehr auf unsicheren Referrer angewiesen + - Funktioniert sowohl bei Speichern als auch Abbrechen + +**Technische Details:** +- Templates prüfen `request.args.get('show_test')` für Navigation +- Hidden Input: `` +- Routes: `show_test = request.form.get('show_test') or request.args.get('show_test')` + +**Geänderte Dateien:** +- `v2_adminpanel/templates/edit_license.html` +- `v2_adminpanel/templates/edit_customer.html` +- `v2_adminpanel/app.py` (edit_license und edit_customer Routen) + **Status:** ✅ Vollständig implementiert \ No newline at end of file diff --git a/THE_ROAD_SO_FAR.md b/THE_ROAD_SO_FAR.md index ae72d82..4bb603d 100644 --- a/THE_ROAD_SO_FAR.md +++ b/THE_ROAD_SO_FAR.md @@ -1,5 +1,5 @@ # THE ROAD SO FAR -**Stand: 09.06.2025, 15:41 MEZ** +**Stand: 09.06.2025, 18:13 MEZ** ## 🚀 Aktueller Status @@ -40,10 +40,51 @@ ## 📋 TODO-Liste ### 1. Admin Panel UI-Verbesserungen -- [ ] **Kombinierte Kunden-Lizenz-Ansicht**: Master-Detail-Layout mit Kundenliste links, Lizenzen rechts + +#### ✅ Erledigt: Konsolidierung Kunden-Lizenz-Verwaltung +**Problem**: +- Kombinierte Ansicht `/customers-licenses` existiert, hat aber Formatierungs- und Funktionsprobleme +- Drei separate Views (`/customers`, `/licenses`, `/customers-licenses`) führen zu Redundanz +- Inkonsistente User Experience + +**Geplante Lösung**: Vollständige Konsolidierung in einer zentralen Ansicht +- `/customers-licenses` wird zur Hauptansicht für alle Kunden- und Lizenzoperationen +- Separate Views `/customers` und `/licenses` werden überflüssig +- Inline-Editing und Bulk-Operationen direkt in der kombinierten Ansicht + +**Finaler Stand** (09.06.2025, 18:13): +- ✅ Bootstrap Icons in base.html eingebunden +- ✅ JavaScript-Fehler behoben (copyToClipboard, Header-Update) +- ✅ Container-Styling vereinheitlicht (container statt container-fluid) +- ✅ Bootstrap JavaScript für Modal-Support hinzugefügt +- ✅ Navigation angepasst - alle Links zeigen auf kombinierte Ansicht +- ✅ Redirects implementiert - `/customers` und `/licenses` leiten auf `/customers-licenses` um +- ✅ Emojis und Button-Text vereinheitlicht (👥 Kunden & Lizenzen) +- ✅ show_test Parameter wird beim Navigieren und Editieren beibehalten + +**Umgesetzte Lösung**: +1. Bootstrap Icons CSS und JS Libraries eingebunden +2. JavaScript-Fehler in customers_licenses.html behoben: + - copyToClipboard mit event.currentTarget + - Header-Update beim AJAX-Kundenwechsel + - Modal-Funktionalität für "Neue Lizenz" +3. Design-Vereinheitlichung: + - Container mit py-5 padding wie andere Seiten + - Konsistente Emojis in Buttons und Titeln + - Export-Dropdown mit gleichen Icons + - Card-Styling mit Schatten +4. Testkunden-Filter Persistenz: + - Checkbox behält Status beim Neuladen + - show_test Parameter in allen URLs weitergegeben + - Redirects nach Editieren behalten Parameter bei +5. Dashboard-Navigation angepasst - Klicks auf Statistiken führen zur kombinierten Ansicht +6. Alte Routes mit Redirects versehen - kein Code-Duplikat mehr + +**Status**: Die Konsolidierung ist vollständig abgeschlossen. Die kombinierte Ansicht ist jetzt die zentrale Stelle für alle Kunden- und Lizenzoperationen mit einheitlichem Design und voller Funktionalität. + +#### Weitere geplante Features - [ ] **Globale Suche**: Eine Suchbox für alles (Kunden, Lizenzen, Keys) mit Autocomplete - [ ] **Expandable Rows**: Details in Tabelle ausklappen ohne Seitenwechsel -- [ ] **Dark Mode**: Dunkles Theme mit System-Preference Detection - [ ] **Batch-Import**: CSV/Excel Upload für Massen-Import mit Validierung - [ ] **Timeline/Calendar View**: Kalenderansicht für Lizenz-Ablaufdaten diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 87ddf1f..bfbf18e 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -1096,12 +1096,43 @@ def api_customers(): search = request.args.get('q', '').strip() page = request.args.get('page', 1, type=int) per_page = 20 + customer_id = request.args.get('id', type=int) conn = get_connection() cur = conn.cursor() + # Einzelnen Kunden per ID abrufen + if customer_id: + cur.execute(""" + SELECT c.id, c.name, c.email, + COUNT(l.id) as license_count + FROM customers c + LEFT JOIN licenses l ON c.id = l.customer_id + WHERE c.id = %s + GROUP BY c.id, c.name, c.email + """, (customer_id,)) + + customer = cur.fetchone() + results = [] + if customer: + results.append({ + 'id': customer[0], + 'text': f"{customer[1]} ({customer[2]})", + 'name': customer[1], + 'email': customer[2], + 'license_count': customer[3] + }) + + cur.close() + conn.close() + + return jsonify({ + 'results': results, + 'pagination': {'more': False} + }) + # SQL Query mit optionaler Suche - if search: + elif search: cur.execute(""" SELECT c.id, c.name, c.email, COUNT(l.id) as license_count @@ -1615,9 +1646,15 @@ def create_license(): cur.close() conn.close() - return redirect("/create") + # Preserve show_test parameter if present + redirect_url = "/create" + if request.args.get('show_test') == 'true': + redirect_url += "?show_test=true" + return redirect(redirect_url) - return render_template("index.html", username=session.get('username')) + # Unterstützung für vorausgewählten Kunden + preselected_customer_id = request.args.get('customer_id', type=int) + return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id) @app.route("/batch", methods=["GET", "POST"]) @login_required @@ -1915,129 +1952,8 @@ def export_batch(): @app.route("/licenses") @login_required def licenses(): - conn = get_connection() - cur = conn.cursor() - - # Parameter - search = request.args.get('search', '').strip() - filter_type = request.args.get('type', '') - filter_status = request.args.get('status', '') - page = request.args.get('page', 1, type=int) - sort = request.args.get('sort', 'valid_until') - order = request.args.get('order', 'desc') - per_page = 20 - - # Whitelist für erlaubte Sortierfelder - allowed_sort_fields = { - 'id': 'l.id', - 'license_key': 'l.license_key', - 'customer': 'c.name', - 'email': 'c.email', - 'type': 'l.license_type', - 'valid_from': 'l.valid_from', - 'valid_until': 'l.valid_until', - 'status': 'status', - 'active': 'l.is_active' - } - - # Validierung - if sort not in allowed_sort_fields: - sort = 'valid_until' - if order not in ['asc', 'desc']: - order = 'desc' - - sort_field = allowed_sort_fields[sort] - - # SQL Query mit optionaler Suche und Filtern - query = """ - SELECT l.id, l.license_key, c.name, c.email, l.license_type, - l.valid_from, l.valid_until, l.is_active, l.is_test, - CASE - WHEN l.is_active = FALSE THEN 'deaktiviert' - 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 - WHERE 1=1 - """ - - params = [] - - # Suchfilter - if search: - query += """ - AND (LOWER(l.license_key) LIKE LOWER(%s) - OR LOWER(c.name) LIKE LOWER(%s) - OR LOWER(c.email) LIKE LOWER(%s)) - """ - search_param = f'%{search}%' - params.extend([search_param, search_param, search_param]) - - # Typ-Filter - if filter_type: - if filter_type == 'test_data': - query += " AND l.is_test = TRUE" - elif filter_type == 'live_data': - query += " AND l.is_test = FALSE" - else: - query += " AND l.license_type = %s AND l.is_test = FALSE" - params.append(filter_type) - - # Status-Filter - if filter_status == 'active': - query += " AND l.valid_until >= CURRENT_DATE AND l.is_active = TRUE" - elif filter_status == 'expiring': - query += " AND l.valid_until >= CURRENT_DATE AND l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.is_active = TRUE" - elif filter_status == 'expired': - query += " AND l.valid_until < CURRENT_DATE" - elif filter_status == 'inactive': - query += " AND l.is_active = FALSE" - - # Gesamtanzahl für Pagination - count_query = "SELECT COUNT(*) FROM (" + query + ") as count_table" - cur.execute(count_query, params) - total = cur.fetchone()[0] - - # Pagination - offset = (page - 1) * per_page - - # Spezialbehandlung für berechnete Felder - if sort == 'status': - # Für Status müssen wir die CASE-Bedingung in ORDER BY wiederholen - query += f""" ORDER BY - CASE - WHEN l.is_active = FALSE THEN 'deaktiviert' - 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 {order.upper()} LIMIT %s OFFSET %s""" - else: - query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s" - - params.extend([per_page, offset]) - - cur.execute(query, params) - licenses = cur.fetchall() - - # Pagination Info - total_pages = (total + per_page - 1) // per_page - - cur.close() - conn.close() - - return render_template("licenses.html", - licenses=licenses, - search=search, - filter_type=filter_type, - filter_status=filter_status, - page=page, - total_pages=total_pages, - total=total, - sort=sort, - order=order, - username=session.get('username')) + # Redirect zur kombinierten Ansicht + return redirect("/customers-licenses") @app.route("/license/edit/", methods=["GET", "POST"]) @login_required @@ -2092,7 +2008,23 @@ def edit_license(license_id): cur.close() conn.close() - return redirect("/licenses") + # Redirect zurück zu customers-licenses mit beibehaltenen Parametern + redirect_url = "/customers-licenses" + + # Behalte show_test Parameter bei (aus Form oder GET-Parameter) + show_test = request.form.get('show_test') or request.args.get('show_test') + if show_test == 'true': + redirect_url += "?show_test=true" + + # Behalte customer_id bei wenn vorhanden + if request.referrer and 'customer_id=' in request.referrer: + import re + match = re.search(r'customer_id=(\d+)', request.referrer) + if match: + connector = "&" if "?" in redirect_url else "?" + redirect_url += f"{connector}customer_id={match.group(1)}" + + return redirect(redirect_url) # Get license data cur.execute(""" @@ -2148,93 +2080,8 @@ def delete_license(license_id): @app.route("/customers") @login_required def customers(): - conn = get_connection() - cur = conn.cursor() - - # Parameter - search = request.args.get('search', '').strip() - page = request.args.get('page', 1, type=int) - sort = request.args.get('sort', 'created_at') - order = request.args.get('order', 'desc') - per_page = 20 - - # Whitelist für erlaubte Sortierfelder - allowed_sort_fields = { - 'id': 'c.id', - 'name': 'c.name', - 'email': 'c.email', - 'created_at': 'c.created_at', - 'licenses': 'license_count', - 'active_licenses': 'active_licenses' - } - - # Validierung - if sort not in allowed_sort_fields: - sort = 'created_at' - if order not in ['asc', 'desc']: - order = 'desc' - - sort_field = allowed_sort_fields[sort] - - # SQL Query mit optionaler Suche - base_query = """ - SELECT c.id, c.name, c.email, c.created_at, c.is_test, - COUNT(l.id) as license_count, - COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses - FROM customers c - LEFT JOIN licenses l ON c.id = l.customer_id - """ - - params = [] - - if search: - base_query += """ - WHERE LOWER(c.name) LIKE LOWER(%s) - OR LOWER(c.email) LIKE LOWER(%s) - """ - search_param = f'%{search}%' - params.extend([search_param, search_param]) - - # Gesamtanzahl für Pagination - count_query = f""" - SELECT COUNT(DISTINCT c.id) - FROM customers c - LEFT JOIN licenses l ON c.id = l.customer_id - {"WHERE LOWER(c.name) LIKE LOWER(%s) OR LOWER(c.email) LIKE LOWER(%s)" if search else ""} - """ - if search: - cur.execute(count_query, params) - else: - cur.execute(count_query) - total = cur.fetchone()[0] - - # Pagination - offset = (page - 1) * per_page - query = base_query + f""" - GROUP BY c.id, c.name, c.email, c.created_at, c.is_test - ORDER BY {sort_field} {order.upper()} - LIMIT %s OFFSET %s - """ - params.extend([per_page, offset]) - - cur.execute(query, params) - customers = cur.fetchall() - - # Pagination Info - total_pages = (total + per_page - 1) // per_page - - cur.close() - conn.close() - - return render_template("customers.html", - customers=customers, - search=search, - page=page, - total_pages=total_pages, - total=total, - sort=sort, - order=order, - username=session.get('username')) + # Redirect zur kombinierten Ansicht + return redirect("/customers-licenses") @app.route("/customer/edit/", methods=["GET", "POST"]) @login_required @@ -2276,11 +2123,23 @@ def edit_customer(customer_id): cur.close() conn.close() - return redirect("/customers") + # Redirect zurück zu customers-licenses mit beibehaltenen Parametern + redirect_url = "/customers-licenses" + + # Behalte show_test Parameter bei (aus Form oder GET-Parameter) + show_test = request.form.get('show_test') or request.args.get('show_test') + if show_test == 'true': + redirect_url += "?show_test=true" + + # Behalte customer_id bei (immer der aktuelle Kunde) + connector = "&" if "?" in redirect_url else "?" + redirect_url += f"{connector}customer_id={customer_id}" + + return redirect(redirect_url) # Get customer data with licenses cur.execute(""" - SELECT id, name, email, is_test FROM customers WHERE id = %s + SELECT id, name, email, created_at, is_test FROM customers WHERE id = %s """, (customer_id,)) customer = cur.fetchone() @@ -2304,7 +2163,7 @@ def edit_customer(customer_id): conn.close() if not customer: - return redirect("/customers") + return redirect("/customers-licenses") return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username')) @@ -2346,6 +2205,247 @@ def delete_customer(customer_id): return redirect("/customers") +@app.route("/customers-licenses") +@login_required +def customers_licenses(): + """Kombinierte Ansicht für Kunden und deren Lizenzen""" + conn = get_connection() + cur = conn.cursor() + + # Hole alle Kunden mit Lizenzstatistiken (inkl. Testkunden wenn gewünscht) + show_test = request.args.get('show_test', 'false').lower() == 'true' + + query = """ + 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 + """ + + if not show_test: + query += " WHERE c.is_test = FALSE" + + query += """ + GROUP BY c.id, c.name, c.email, c.created_at + ORDER BY c.name + """ + + cur.execute(query) + customers = cur.fetchall() + + # Hole ersten Kunden für initiale Anzeige (falls vorhanden) + selected_customer_id = request.args.get('customer_id', type=int) + licenses = [] + selected_customer = None + + if customers: + if not selected_customer_id: + selected_customer_id = customers[0][0] # Erster Kunde + + # Hole Daten des ausgewählten Kunden + for customer in customers: + if customer[0] == selected_customer_id: + selected_customer = customer + break + + # Hole Lizenzen des ausgewählten Kunden + if selected_customer: + cur.execute(""" + SELECT + l.id, + l.license_key, + l.license_type, + l.valid_from, + l.valid_until, + l.is_active, + CASE + WHEN l.is_active = FALSE THEN 'deaktiviert' + 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, + l.domain_count, + l.ipv4_count, + l.phone_count + FROM licenses l + WHERE l.customer_id = %s + ORDER BY l.created_at DESC + """, (selected_customer_id,)) + licenses = cur.fetchall() + + cur.close() + conn.close() + + return render_template("customers_licenses.html", + customers=customers, + selected_customer=selected_customer, + selected_customer_id=selected_customer_id, + licenses=licenses, + show_test=show_test) + +@app.route("/api/customer//licenses") +@login_required +def api_customer_licenses(customer_id): + """API-Endpoint für AJAX-Abruf der Lizenzen eines Kunden""" + conn = get_connection() + cur = conn.cursor() + + # Hole Lizenzen des Kunden + cur.execute(""" + SELECT + l.id, + l.license_key, + l.license_type, + l.valid_from, + l.valid_until, + l.is_active, + CASE + WHEN l.is_active = FALSE THEN 'deaktiviert' + 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, + l.domain_count, + l.ipv4_count, + l.phone_count + FROM licenses l + WHERE l.customer_id = %s + ORDER BY l.created_at DESC + """, (customer_id,)) + + licenses = [] + for row in cur.fetchall(): + licenses.append({ + 'id': row[0], + 'license_key': row[1], + 'license_type': row[2], + 'valid_from': row[3].strftime('%d.%m.%Y') if row[3] else '', + 'valid_until': row[4].strftime('%d.%m.%Y') if row[4] else '', + 'is_active': row[5], + 'status': row[6], + 'domain_count': row[7], + 'ipv4_count': row[8], + 'phone_count': row[9] + }) + + cur.close() + conn.close() + + return jsonify({ + 'success': True, + 'licenses': licenses, + 'count': len(licenses) + }) + +@app.route("/api/customer//quick-stats") +@login_required +def api_customer_quick_stats(customer_id): + """API-Endpoint für Schnellstatistiken eines Kunden""" + conn = get_connection() + cur = conn.cursor() + + # Hole Kundenstatistiken + cur.execute(""" + SELECT + 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, + COUNT(CASE WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.valid_until >= CURRENT_DATE THEN 1 END) as expiring_soon + FROM licenses l + WHERE l.customer_id = %s + """, (customer_id,)) + + stats = cur.fetchone() + + cur.close() + conn.close() + + return jsonify({ + 'success': True, + 'stats': { + 'total': stats[0], + 'active': stats[1], + 'expired': stats[2], + 'expiring_soon': stats[3] + } + }) + +@app.route("/api/license//quick-edit", methods=['POST']) +@login_required +def api_license_quick_edit(license_id): + """API-Endpoint für schnelle Lizenz-Bearbeitung""" + conn = get_connection() + cur = conn.cursor() + + try: + data = request.get_json() + + # Hole alte Werte für Audit-Log + cur.execute(""" + SELECT is_active, valid_until, license_type + FROM licenses WHERE id = %s + """, (license_id,)) + old_values = cur.fetchone() + + if not old_values: + return jsonify({'success': False, 'error': 'Lizenz nicht gefunden'}), 404 + + # Update-Felder vorbereiten + updates = [] + params = [] + new_values = {} + + if 'is_active' in data: + updates.append("is_active = %s") + params.append(data['is_active']) + new_values['is_active'] = data['is_active'] + + if 'valid_until' in data: + updates.append("valid_until = %s") + params.append(data['valid_until']) + new_values['valid_until'] = data['valid_until'] + + if 'license_type' in data: + updates.append("license_type = %s") + params.append(data['license_type']) + new_values['license_type'] = data['license_type'] + + if updates: + params.append(license_id) + cur.execute(f""" + UPDATE licenses + SET {', '.join(updates)} + WHERE id = %s + """, params) + + conn.commit() + + # Audit-Log + log_audit('UPDATE', 'license', license_id, + old_values={ + 'is_active': old_values[0], + 'valid_until': old_values[1].isoformat() if old_values[1] else None, + 'license_type': old_values[2] + }, + new_values=new_values) + + cur.close() + conn.close() + + return jsonify({'success': True}) + + except Exception as e: + conn.rollback() + cur.close() + conn.close() + return jsonify({'success': False, 'error': str(e)}), 500 + @app.route("/sessions") @login_required def sessions(): diff --git a/v2_adminpanel/templates/base.html b/v2_adminpanel/templates/base.html index 0ab8810..dd735d6 100644 --- a/v2_adminpanel/templates/base.html +++ b/v2_adminpanel/templates/base.html @@ -5,6 +5,7 @@ {% block title %}Admin Panel{% endblock %} - Lizenzverwaltung + {% block extra_css %}{% endblock %} @@ -379,8 +380,30 @@ // Initial Heartbeat extendSession(); + // Preserve show_test parameter across navigation + function preserveShowTestParameter() { + const urlParams = new URLSearchParams(window.location.search); + const showTest = urlParams.get('show_test'); + + if (showTest === 'true') { + // Update all internal links to include show_test parameter + document.querySelectorAll('a[href^="/"]').forEach(link => { + const href = link.getAttribute('href'); + // Skip if already has parameters or is just a fragment + if (!href.includes('?') && !href.startsWith('#')) { + link.setAttribute('href', href + '?show_test=true'); + } else if (href.includes('?') && !href.includes('show_test=')) { + link.setAttribute('href', href + '&show_test=true'); + } + }); + } + } + // Client-side table sorting document.addEventListener('DOMContentLoaded', function() { + // Preserve show_test parameter on page load + preserveShowTestParameter(); + // Initialize all sortable tables const sortableTables = document.querySelectorAll('.sortable-table'); @@ -455,6 +478,9 @@ } + + + {% block extra_js %}{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/templates/customers.html b/v2_adminpanel/templates/customers.html index 3414ea8..4e340d4 100644 --- a/v2_adminpanel/templates/customers.html +++ b/v2_adminpanel/templates/customers.html @@ -28,6 +28,9 @@

Kundenverwaltung

+ + Kombinierte Ansicht + 📊 Dashboard ➕ Neue Lizenz 🔑 Batch-Lizenzen diff --git a/v2_adminpanel/templates/customers_licenses.html b/v2_adminpanel/templates/customers_licenses.html new file mode 100644 index 0000000..ebc4d68 --- /dev/null +++ b/v2_adminpanel/templates/customers_licenses.html @@ -0,0 +1,443 @@ +{% extends "base.html" %} + +{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %} + +{% block content %} +
+
+

👥 Kunden & Lizenzen

+ +
+ +
+ +
+
+
+
+ Kunden + {{ customers|length if customers else 0 }} +
+
+
+ +
+ +
+ + +
+
+ + +
+ {% if customers %} + {% for customer in customers %} +
+
+
+
{{ customer[1] }}
+ {{ customer[2] }} +
+
+ {{ customer[4] }} + {% if customer[5] > 0 %} + {{ customer[5] }} + {% endif %} + {% if customer[6] > 0 %} + {{ customer[6] }} + {% endif %} +
+
+
+ {% endfor %} + {% else %} +
+ +

Keine Kunden vorhanden

+ Erstellen Sie eine neue Lizenz, um automatisch einen Kunden anzulegen. + + Neue Lizenz erstellen + +
+ {% endif %} +
+
+
+
+ + +
+
+
+ {% if selected_customer %} +
+
+
{{ selected_customer[1] }}
+ {{ selected_customer[2] }} +
+
+ + Bearbeiten + + +
+
+ {% else %} +
Wählen Sie einen Kunden aus
+ {% endif %} +
+
+
+ {% if selected_customer %} + {% if licenses %} +
+ + + + + + + + + + + + + + {% for license in licenses %} + + + + + + + + + + {% endfor %} + +
LizenzschlüsselTypGültig vonGültig bisStatusRessourcenAktionen
+ {{ license[1] }} + + + + {{ license[2]|upper }} + + {{ license[3].strftime('%d.%m.%Y') if license[3] else '-' }}{{ license[4].strftime('%d.%m.%Y') if license[4] else '-' }} + + {{ license[6] }} + + + {% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %} + {% if license[8] > 0 %}📡 {{ license[8] }}{% endif %} + {% if license[9] > 0 %}📱 {{ license[9] }}{% endif %} + +
+ + + + +
+
+
+ {% else %} +
+ +

Keine Lizenzen für diesen Kunden vorhanden

+ +
+ {% endif %} + {% else %} +
+ +

Wählen Sie einen Kunden aus der Liste aus

+
+ {% endif %} +
+
+
+
+
+
+ + + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/templates/customers_licenses_old.html b/v2_adminpanel/templates/customers_licenses_old.html new file mode 100644 index 0000000..1a3b6ba --- /dev/null +++ b/v2_adminpanel/templates/customers_licenses_old.html @@ -0,0 +1,491 @@ +{% extends "base.html" %} + +{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %} + +{% block content %} +
+
+

Kunden & Lizenzen

+ +
+ +
+ +
+
+
+
+ Kunden + {{ customers|length if customers else 0 }} +
+
+
+ +
+ +
+ + +
+
+ + +
+ {% if customers %} + {% for customer in customers %} +
+
+
+
{{ customer[1] }}
+ {{ customer[2] }} +
+
+ {{ customer[4] }} + {% if customer[5] > 0 %} + {{ customer[5] }} + {% endif %} + {% if customer[6] > 0 %} + {{ customer[6] }} + {% endif %} +
+
+
+ {% endfor %} + {% else %} +
+ +

Keine Kunden vorhanden

+ Erstellen Sie eine neue Lizenz, um automatisch einen Kunden anzulegen. + + Neue Lizenz erstellen + +
+ {% endif %} +
+
+
+
+ + +
+
+
+ {% if selected_customer %} +
+
+
{{ selected_customer[1] }}
+ {{ selected_customer[2] }} +
+
+ + Bearbeiten + + +
+
+ {% else %} +
Wählen Sie einen Kunden aus
+ {% endif %} +
+
+
+ {% if selected_customer %} + {% if licenses %} +
+ + + + + + + + + + + + + + {% for license in licenses %} + + + + + + + + + + {% endfor %} + +
LizenzschlüsselTypGültig vonGültig bisStatusRessourcenAktionen
+ {{ license[1] }} + + + + {{ license[2]|upper }} + + {{ license[3].strftime('%d.%m.%Y') if license[3] else '-' }}{{ license[4].strftime('%d.%m.%Y') if license[4] else '-' }} + + {{ license[6] }} + + + {% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %} + {% if license[8] > 0 %}📡 {{ license[8] }}{% endif %} + {% if license[9] > 0 %}📱 {{ license[9] }}{% endif %} + +
+ + + + +
+
+
+ {% else %} +
+ +

Keine Lizenzen für diesen Kunden vorhanden

+ +
+ {% endif %} + {% else %} +
+ +

Wählen Sie einen Kunden aus der Liste aus

+
+ {% endif %} +
+
+
+
+
+
+ + + + + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/templates/dashboard.html b/v2_adminpanel/templates/dashboard.html index ebc1afa..f898f0a 100644 --- a/v2_adminpanel/templates/dashboard.html +++ b/v2_adminpanel/templates/dashboard.html @@ -68,6 +68,7 @@

Dashboard

+ 👥 Kunden & Lizenzen ➕ Neue Lizenz 🔑 Batch-Lizenzen 📝 Log @@ -77,7 +78,7 @@
- +
- +
+ {% if request.args.get('show_test') == 'true' %} + + {% endif %}
@@ -31,7 +34,7 @@
- +
diff --git a/v2_adminpanel/templates/edit_license.html b/v2_adminpanel/templates/edit_license.html index 905887b..5281c4a 100644 --- a/v2_adminpanel/templates/edit_license.html +++ b/v2_adminpanel/templates/edit_license.html @@ -8,13 +8,16 @@

Lizenz bearbeiten

+ {% if request.args.get('show_test') == 'true' %} + + {% endif %}
@@ -64,7 +67,7 @@
- Abbrechen + Abbrechen
diff --git a/v2_adminpanel/templates/index.html b/v2_adminpanel/templates/index.html index 635e8d0..1f80db5 100644 --- a/v2_adminpanel/templates/index.html +++ b/v2_adminpanel/templates/index.html @@ -317,6 +317,24 @@ document.addEventListener('DOMContentLoaded', function() { } }); + // Vorausgewählten Kunden setzen (falls von kombinierter Ansicht kommend) + {% if preselected_customer_id %} + // Lade Kundendetails und setze Auswahl + fetch('/api/customers?id={{ preselected_customer_id }}') + .then(response => response.json()) + .then(data => { + if (data.results && data.results.length > 0) { + const customer = data.results[0]; + // Erstelle Option und setze sie als ausgewählt + const option = new Option(customer.text, customer.id, true, true); + $('#customerSelect').append(option).trigger('change'); + // Verstecke die Eingabefelder + document.getElementById('customerNameDiv').style.display = 'none'; + document.getElementById('emailDiv').style.display = 'none'; + } + }); + {% endif %} + // Event Handler für Kundenauswahl $('#customerSelect').on('select2:select', function (e) { const selectedValue = e.params.data.id; diff --git a/v2_adminpanel/templates/licenses.html b/v2_adminpanel/templates/licenses.html index c53b98e..7095c7a 100644 --- a/v2_adminpanel/templates/licenses.html +++ b/v2_adminpanel/templates/licenses.html @@ -31,6 +31,9 @@