diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fdf5767..ea8527f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -44,7 +44,8 @@ "Bash(docker-compose restart:*)", "Bash(find:*)", "Bash(docker network:*)", - "Bash(curl:*)" + "Bash(curl:*)", + "Bash(find:*)" ], "deny": [] } diff --git a/JOURNAL.md b/JOURNAL.md index 4990ec9..eb0b5aa 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -1028,4 +1028,39 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier - ✅ Export als CSV - ✅ Copy-to-Clipboard Funktionalität - ✅ Audit-Log-Integration -- ✅ Navigation aktualisiert \ No newline at end of file +- ✅ Navigation aktualisiert + +## 2025-01-06: Implementierung Searchable Dropdown für Kundenauswahl + +**Problem:** +- Bei der Lizenzerstellung wurde immer ein neuer Kunde angelegt +- Keine Möglichkeit, Lizenzen für bestehende Kunden zu erstellen +- Bei vielen Kunden wäre ein normales Dropdown unübersichtlich + +**Lösung:** +1. **Select2 Library** für searchable Dropdown integriert +2. **API-Endpoint `/api/customers`** für die Kundensuche erstellt +3. **Frontend angepasst:** + - Searchable Dropdown mit Live-Suche + - Option "Neuer Kunde" im Dropdown + - Eingabefelder erscheinen nur bei "Neuer Kunde" +4. **Backend-Logik verbessert:** + - Prüfung ob neuer oder bestehender Kunde + - E-Mail-Duplikatsprüfung vor Kundenerstellung + - Separate Audit-Logs für Kunde und Lizenz +5. **Datenbank:** + - UNIQUE Constraint auf E-Mail-Spalte hinzugefügt + +**Änderungen:** +- `app.py`: Neuer API-Endpoint `/api/customers`, angepasste Routes `/create` und `/batch` +- `base.html`: Select2 CSS und JS eingebunden +- `index.html`: Kundenauswahl mit Select2 implementiert +- `batch_form.html`: Kundenauswahl mit Select2 implementiert +- `init.sql`: UNIQUE Constraint für E-Mail + +**Status:** +- ✅ API-Endpoint funktioniert mit Pagination +- ✅ Select2 Dropdown mit Suchfunktion +- ✅ Neue/bestehende Kunden können ausgewählt werden +- ✅ E-Mail-Duplikate werden verhindert +- ✅ Sowohl Einzellizenz als auch Batch unterstützt \ No newline at end of file diff --git a/v2/cookies.txt b/v2/cookies.txt index 5a9d927..c31d989 100644 --- a/v2/cookies.txt +++ b/v2/cookies.txt @@ -2,4 +2,3 @@ # https://curl.se/docs/http-cookies.html # This file was generated by libcurl! Edit at your own risk. -#HttpOnly_localhost FALSE / FALSE 1749330711 admin_session kL1GAl-qrgTLBwfFpeQLngAfFne2ehrnKbWE3M3awqE diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 2127cf3..558254b 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -764,6 +764,86 @@ def api_generate_key(): 'error': 'Fehler bei der Key-Generierung' }), 500 +@app.route("/api/customers", methods=['GET']) +@login_required +def api_customers(): + """API Endpoint für die Kundensuche mit Select2""" + try: + # Suchparameter + search = request.args.get('q', '').strip() + page = request.args.get('page', 1, type=int) + per_page = 20 + + conn = get_connection() + cur = conn.cursor() + + # SQL Query mit optionaler Suche + if search: + 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 LOWER(c.name) LIKE LOWER(%s) + OR LOWER(c.email) LIKE LOWER(%s) + GROUP BY c.id, c.name, c.email + ORDER BY c.name + LIMIT %s OFFSET %s + """, (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page)) + else: + 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 + GROUP BY c.id, c.name, c.email + ORDER BY c.name + LIMIT %s OFFSET %s + """, (per_page, (page - 1) * per_page)) + + customers = cur.fetchall() + + # Format für Select2 + results = [] + for customer in customers: + results.append({ + 'id': customer[0], + 'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)", + 'name': customer[1], + 'email': customer[2], + 'license_count': customer[3] + }) + + # Gesamtanzahl für Pagination + if search: + cur.execute(""" + SELECT COUNT(*) FROM customers + WHERE LOWER(name) LIKE LOWER(%s) + OR LOWER(email) LIKE LOWER(%s) + """, (f'%{search}%', f'%{search}%')) + else: + cur.execute("SELECT COUNT(*) FROM customers") + + total_count = cur.fetchone()[0] + + cur.close() + conn.close() + + # Select2 Response Format + return jsonify({ + 'results': results, + 'pagination': { + 'more': (page * per_page) < total_count + } + }) + + except Exception as e: + logging.error(f"Fehler bei Kundensuche: {str(e)}") + return jsonify({ + 'results': [], + 'error': 'Fehler bei der Kundensuche' + }), 500 + @app.route("/") @login_required def dashboard(): @@ -931,8 +1011,7 @@ def dashboard(): @login_required def create_license(): if request.method == "POST": - name = request.form["customer_name"] - email = request.form["email"] + customer_id = request.form.get("customer_id") license_key = request.form["license_key"].upper() # Immer Großbuchstaben license_type = request.form["license_type"] valid_from = request.form["valid_from"] @@ -946,36 +1025,76 @@ def create_license(): conn = get_connection() cur = conn.cursor() - # Kunde einfügen (falls nicht vorhanden) - cur.execute(""" - INSERT INTO customers (name, email, created_at) - VALUES (%s, %s, NOW()) - RETURNING id - """, (name, email)) - customer_id = cur.fetchone()[0] + try: + # Prüfe ob neuer Kunde oder bestehender + if customer_id == "new": + # Neuer Kunde + name = request.form.get("customer_name") + email = request.form.get("email") + + if not name: + flash('Kundenname ist erforderlich!', 'error') + return redirect(url_for('create_license')) + + # Prüfe ob E-Mail bereits existiert + if email: + cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,)) + existing = cur.fetchone() + if existing: + flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error') + return redirect(url_for('create_license')) + + # Kunde einfügen + cur.execute(""" + INSERT INTO customers (name, email, created_at) + VALUES (%s, %s, NOW()) + RETURNING id + """, (name, email)) + customer_id = cur.fetchone()[0] + customer_info = {'name': name, 'email': email} + + # Audit-Log für neuen Kunden + log_audit('CREATE', 'customer', customer_id, + new_values={'name': name, 'email': email}) + else: + # Bestehender Kunde - hole Infos für Audit-Log + cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,)) + customer_data = cur.fetchone() + if not customer_data: + flash('Kunde nicht gefunden!', 'error') + return redirect(url_for('create_license')) + customer_info = {'name': customer_data[0], 'email': customer_data[1]} - # Lizenz hinzufügen - cur.execute(""" - INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active) - VALUES (%s, %s, %s, %s, %s, TRUE) - RETURNING id - """, (license_key, customer_id, license_type, valid_from, valid_until)) - license_id = cur.fetchone()[0] + # Lizenz hinzufügen + cur.execute(""" + INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active) + VALUES (%s, %s, %s, %s, %s, TRUE) + RETURNING id + """, (license_key, customer_id, license_type, valid_from, valid_until)) + license_id = cur.fetchone()[0] - conn.commit() - - # Audit-Log - log_audit('CREATE', 'license', license_id, - new_values={ - 'license_key': license_key, - 'customer_name': name, - 'customer_email': email, - 'license_type': license_type, - 'valid_from': valid_from, - 'valid_until': valid_until - }) - cur.close() - conn.close() + conn.commit() + + # Audit-Log + log_audit('CREATE', 'license', license_id, + new_values={ + 'license_key': license_key, + 'customer_name': customer_info['name'], + 'customer_email': customer_info['email'], + 'license_type': license_type, + 'valid_from': valid_from, + 'valid_until': valid_until + }) + + flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success') + + except Exception as e: + conn.rollback() + logging.error(f"Fehler beim Erstellen der Lizenz: {str(e)}") + flash('Fehler beim Erstellen der Lizenz!', 'error') + finally: + cur.close() + conn.close() return redirect("/create") @@ -987,8 +1106,7 @@ 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"] + customer_id = request.form.get("customer_id") license_type = request.form["license_type"] quantity = int(request.form["quantity"]) valid_from = request.form["valid_from"] @@ -1003,14 +1121,44 @@ def batch_licenses(): 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] + # Prüfe ob neuer Kunde oder bestehender + if customer_id == "new": + # Neuer Kunde + name = request.form.get("customer_name") + email = request.form.get("email") + + if not name: + flash('Kundenname ist erforderlich!', 'error') + return redirect(url_for('batch_licenses')) + + # Prüfe ob E-Mail bereits existiert + if email: + cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,)) + existing = cur.fetchone() + if existing: + flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error') + return redirect(url_for('batch_licenses')) + + # Kunde einfügen + cur.execute(""" + INSERT INTO customers (name, email, created_at) + VALUES (%s, %s, NOW()) + RETURNING id + """, (name, email)) + customer_id = cur.fetchone()[0] + + # Audit-Log für neuen Kunden + log_audit('CREATE', 'customer', customer_id, + new_values={'name': name, 'email': email}) + else: + # Bestehender Kunde - hole Infos + cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,)) + customer_data = cur.fetchone() + if not customer_data: + flash('Kunde nicht gefunden!', 'error') + return redirect(url_for('batch_licenses')) + name = customer_data[0] + email = customer_data[1] # Lizenzen generieren und speichern generated_licenses = [] diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql index dc7b10d..c363219 100644 --- a/v2_adminpanel/init.sql +++ b/v2_adminpanel/init.sql @@ -5,7 +5,8 @@ CREATE TABLE IF NOT EXISTS customers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_email UNIQUE (email) ); CREATE TABLE IF NOT EXISTS licenses ( diff --git a/v2_adminpanel/templates/base.html b/v2_adminpanel/templates/base.html index a21a67d..22b7687 100644 --- a/v2_adminpanel/templates/base.html +++ b/v2_adminpanel/templates/base.html @@ -5,6 +5,8 @@