Kunden & Lizenzen - Part1
Dieser Commit ist enthalten in:
266
JOURNAL.md
266
JOURNAL.md
@@ -2188,6 +2188,88 @@ docker-compose up -d
|
|||||||
|
|
||||||
**Status:** ✅ Alle Lizenzschlüssel erfolgreich migriert
|
**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/<id>/licenses`, `/api/customer/<id>/quick-stats`
|
||||||
|
- API-Endpoint `/api/license/<id>/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
|
### 2025-06-09: Test-Flag für Lizenzen implementiert
|
||||||
|
|
||||||
**Ziel:**
|
**Ziel:**
|
||||||
@@ -2277,4 +2359,188 @@ UPDATE resource_pools SET is_test = TRUE; -- 20 Ressourcen
|
|||||||
- Test-Ressourcen werden nur Test-Lizenzen zugewiesen
|
- Test-Ressourcen werden nur Test-Lizenzen zugewiesen
|
||||||
- Alle bestehenden Daten sind jetzt als Test markiert
|
- 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: `<input type="hidden" name="show_test" value="true">`
|
||||||
|
- 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
|
**Status:** ✅ Vollständig implementiert
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# THE ROAD SO FAR
|
# THE ROAD SO FAR
|
||||||
**Stand: 09.06.2025, 15:41 MEZ**
|
**Stand: 09.06.2025, 18:13 MEZ**
|
||||||
|
|
||||||
## 🚀 Aktueller Status
|
## 🚀 Aktueller Status
|
||||||
|
|
||||||
@@ -40,10 +40,51 @@
|
|||||||
## 📋 TODO-Liste
|
## 📋 TODO-Liste
|
||||||
|
|
||||||
### 1. Admin Panel UI-Verbesserungen
|
### 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
|
- [ ] **Globale Suche**: Eine Suchbox für alles (Kunden, Lizenzen, Keys) mit Autocomplete
|
||||||
- [ ] **Expandable Rows**: Details in Tabelle ausklappen ohne Seitenwechsel
|
- [ ] **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
|
- [ ] **Batch-Import**: CSV/Excel Upload für Massen-Import mit Validierung
|
||||||
- [ ] **Timeline/Calendar View**: Kalenderansicht für Lizenz-Ablaufdaten
|
- [ ] **Timeline/Calendar View**: Kalenderansicht für Lizenz-Ablaufdaten
|
||||||
|
|
||||||
|
|||||||
@@ -1096,12 +1096,43 @@ def api_customers():
|
|||||||
search = request.args.get('q', '').strip()
|
search = request.args.get('q', '').strip()
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
per_page = 20
|
per_page = 20
|
||||||
|
customer_id = request.args.get('id', type=int)
|
||||||
|
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
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
|
# SQL Query mit optionaler Suche
|
||||||
if search:
|
elif search:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT c.id, c.name, c.email,
|
SELECT c.id, c.name, c.email,
|
||||||
COUNT(l.id) as license_count
|
COUNT(l.id) as license_count
|
||||||
@@ -1615,9 +1646,15 @@ def create_license():
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.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"])
|
@app.route("/batch", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -1915,129 +1952,8 @@ def export_batch():
|
|||||||
@app.route("/licenses")
|
@app.route("/licenses")
|
||||||
@login_required
|
@login_required
|
||||||
def licenses():
|
def licenses():
|
||||||
conn = get_connection()
|
# Redirect zur kombinierten Ansicht
|
||||||
cur = conn.cursor()
|
return redirect("/customers-licenses")
|
||||||
|
|
||||||
# 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'))
|
|
||||||
|
|
||||||
@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
|
@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2092,7 +2008,23 @@ def edit_license(license_id):
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.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
|
# Get license data
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
@@ -2148,93 +2080,8 @@ def delete_license(license_id):
|
|||||||
@app.route("/customers")
|
@app.route("/customers")
|
||||||
@login_required
|
@login_required
|
||||||
def customers():
|
def customers():
|
||||||
conn = get_connection()
|
# Redirect zur kombinierten Ansicht
|
||||||
cur = conn.cursor()
|
return redirect("/customers-licenses")
|
||||||
|
|
||||||
# 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'))
|
|
||||||
|
|
||||||
@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
|
@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2276,11 +2123,23 @@ def edit_customer(customer_id):
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.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
|
# Get customer data with licenses
|
||||||
cur.execute("""
|
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_id,))
|
||||||
|
|
||||||
customer = cur.fetchone()
|
customer = cur.fetchone()
|
||||||
@@ -2304,7 +2163,7 @@ def edit_customer(customer_id):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if not customer:
|
if not customer:
|
||||||
return redirect("/customers")
|
return redirect("/customers-licenses")
|
||||||
|
|
||||||
return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username'))
|
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")
|
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/<int:customer_id>/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/<int:customer_id>/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/<int:license_id>/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")
|
@app.route("/sessions")
|
||||||
@login_required
|
@login_required
|
||||||
def sessions():
|
def sessions():
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}Admin Panel{% endblock %} - Lizenzverwaltung</title>
|
<title>{% block title %}Admin Panel{% endblock %} - Lizenzverwaltung</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
@@ -379,8 +380,30 @@
|
|||||||
// Initial Heartbeat
|
// Initial Heartbeat
|
||||||
extendSession();
|
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
|
// Client-side table sorting
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Preserve show_test parameter on page load
|
||||||
|
preserveShowTestParameter();
|
||||||
|
|
||||||
// Initialize all sortable tables
|
// Initialize all sortable tables
|
||||||
const sortableTables = document.querySelectorAll('.sortable-table');
|
const sortableTables = document.querySelectorAll('.sortable-table');
|
||||||
|
|
||||||
@@ -455,6 +478,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -28,6 +28,9 @@
|
|||||||
<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>Kundenverwaltung</h2>
|
<h2>Kundenverwaltung</h2>
|
||||||
<div>
|
<div>
|
||||||
|
<a href="/customers-licenses" class="btn btn-success">
|
||||||
|
<i class="bi bi-layout-split"></i> Kombinierte Ansicht
|
||||||
|
</a>
|
||||||
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
||||||
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
||||||
|
|||||||
443
v2_adminpanel/templates/customers_licenses.html
Normale Datei
443
v2_adminpanel/templates/customers_licenses.html
Normale Datei
@@ -0,0 +1,443 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>👥 Kunden & Lizenzen</h2>
|
||||||
|
<div>
|
||||||
|
<a href="/create" class="btn btn-success">➕ Neue Lizenz</a>
|
||||||
|
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
|
📥 Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=excel">📊 Excel (.xlsx)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=csv">📄 CSV (.csv)</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Kundenliste (Links) -->
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-people"></i> Kunden
|
||||||
|
<span class="badge bg-secondary float-end">{{ customers|length if customers else 0 }}</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<!-- Suchfeld -->
|
||||||
|
<div class="p-3 border-bottom">
|
||||||
|
<input type="text" class="form-control mb-2" id="customerSearch"
|
||||||
|
placeholder="Kunde suchen..." autocomplete="off">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="showTestCustomers"
|
||||||
|
{% if request.args.get('show_test', 'false').lower() == 'true' %}checked{% endif %}
|
||||||
|
onchange="toggleTestCustomers()">
|
||||||
|
<label class="form-check-label" for="showTestCustomers">
|
||||||
|
<small class="text-muted">Testkunden anzeigen</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kundenliste -->
|
||||||
|
<div class="customer-list" style="max-height: 600px; overflow-y: auto;">
|
||||||
|
{% if customers %}
|
||||||
|
{% for customer in customers %}
|
||||||
|
<div class="customer-item p-3 border-bottom {% if customer[0] == selected_customer_id %}active{% endif %}"
|
||||||
|
data-customer-id="{{ customer[0] }}"
|
||||||
|
data-customer-name="{{ customer[1]|lower }}"
|
||||||
|
data-customer-email="{{ customer[2]|lower }}"
|
||||||
|
onclick="loadCustomerLicenses({{ customer[0] }})"
|
||||||
|
style="cursor: pointer;">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">{{ customer[1] }}</h6>
|
||||||
|
<small class="text-muted">{{ customer[2] }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-primary">{{ customer[4] }}</span>
|
||||||
|
{% if customer[5] > 0 %}
|
||||||
|
<span class="badge bg-success">{{ customer[5] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if customer[6] > 0 %}
|
||||||
|
<span class="badge bg-danger">{{ customer[6] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
<p class="mt-3 mb-2">Keine Kunden vorhanden</p>
|
||||||
|
<small class="d-block mb-3">Erstellen Sie eine neue Lizenz, um automatisch einen Kunden anzulegen.</small>
|
||||||
|
<a href="/create" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Neue Lizenz erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lizenzdetails (Rechts) -->
|
||||||
|
<div class="col-md-8 col-lg-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
{% if selected_customer %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">{{ selected_customer[1] }}</h5>
|
||||||
|
<small class="text-muted">{{ selected_customer[2] }}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/customer/edit/{{ selected_customer[0] }}?ref=customers-licenses{% if request.args.get('show_test') %}&show_test=true{% endif %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
|
||||||
|
<i class="bi bi-plus"></i> Neue Lizenz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h5 class="mb-0">Wählen Sie einen Kunden aus</h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="licenseContainer">
|
||||||
|
{% if selected_customer %}
|
||||||
|
{% if licenses %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Lizenzschlüssel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Gültig von</th>
|
||||||
|
<th>Gültig bis</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Ressourcen</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for license in licenses %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>{{ license[1] }}</code>
|
||||||
|
<button class="btn btn-sm btn-link" onclick="copyToClipboard('{{ license[1] }}')">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if license[2] == 'full' %}bg-primary{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ license[2]|upper }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ license[3].strftime('%d.%m.%Y') if license[3] else '-' }}</td>
|
||||||
|
<td>{{ license[4].strftime('%d.%m.%Y') if license[4] else '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge
|
||||||
|
{% if license[6] == 'aktiv' %}bg-success
|
||||||
|
{% elif license[6] == 'läuft bald ab' %}bg-warning
|
||||||
|
{% elif license[6] == 'abgelaufen' %}bg-danger
|
||||||
|
{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ license[6] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %}
|
||||||
|
{% if license[8] > 0 %}📡 {{ license[8] }}{% endif %}
|
||||||
|
{% if license[9] > 0 %}📱 {{ license[9] }}{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus({{ license[0] }}, {{ license[5] }})">
|
||||||
|
<i class="bi bi-power"></i>
|
||||||
|
</button>
|
||||||
|
<a href="/license/edit/{{ license[0] }}{% if request.args.get('show_test') %}?ref=customers-licenses&show_test=true{% endif %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
|
||||||
|
<button class="btn btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
|
||||||
|
<i class="bi bi-plus"></i> Erste Lizenz erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-arrow-left text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">Wählen Sie einen Kunden aus der Liste aus</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal für neue Lizenz -->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.customer-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.customer-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left-color: #dee2e6;
|
||||||
|
}
|
||||||
|
.customer-item.active {
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
border-left-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||||
|
}
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Globale Variablen
|
||||||
|
let currentCustomerId = {{ selected_customer_id or 'null' }};
|
||||||
|
|
||||||
|
// Kundensuche
|
||||||
|
document.getElementById('customerSearch').addEventListener('input', function(e) {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
const customerItems = document.querySelectorAll('.customer-item');
|
||||||
|
|
||||||
|
customerItems.forEach(item => {
|
||||||
|
const name = item.dataset.customerName;
|
||||||
|
const email = item.dataset.customerEmail;
|
||||||
|
if (name.includes(searchTerm) || email.includes(searchTerm)) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lade Lizenzen eines Kunden
|
||||||
|
function loadCustomerLicenses(customerId) {
|
||||||
|
// Aktiven Status aktualisieren
|
||||||
|
document.querySelectorAll('.customer-item').forEach(item => {
|
||||||
|
item.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.querySelector(`[data-customer-id="${customerId}"]`).classList.add('active');
|
||||||
|
|
||||||
|
// URL aktualisieren ohne Reload (behalte show_test Parameter)
|
||||||
|
const currentUrl = new URL(window.location);
|
||||||
|
currentUrl.searchParams.set('customer_id', customerId);
|
||||||
|
window.history.pushState({}, '', currentUrl.toString());
|
||||||
|
|
||||||
|
// Lade Lizenzen via AJAX
|
||||||
|
const container = document.getElementById('licenseContainer');
|
||||||
|
const cardHeader = document.querySelector('.card-header.bg-light');
|
||||||
|
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
|
||||||
|
|
||||||
|
fetch(`/api/customer/${customerId}/licenses`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Update header with customer info
|
||||||
|
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
|
||||||
|
const customerName = customerItem.querySelector('h6').textContent;
|
||||||
|
const customerEmail = customerItem.querySelector('small').textContent;
|
||||||
|
|
||||||
|
cardHeader.innerHTML = `
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">${customerName}</h5>
|
||||||
|
<small class="text-muted">${customerEmail}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/customer/edit/${customerId}?ref=customers-licenses${window.location.search ? '&' + window.location.search.substring(1) : ''}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal(${customerId})">
|
||||||
|
<i class="bi bi-plus"></i> Neue Lizenz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateLicenseView(customerId, data.licenses);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
container.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Lizenzen</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Lizenzansicht
|
||||||
|
function updateLicenseView(customerId, licenses) {
|
||||||
|
currentCustomerId = customerId;
|
||||||
|
const container = document.getElementById('licenseContainer');
|
||||||
|
|
||||||
|
if (licenses.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
|
||||||
|
<button class="btn btn-success" onclick="showNewLicenseModal(${customerId})">
|
||||||
|
<i class="bi bi-plus"></i> Erste Lizenz erstellen
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Lizenzschlüssel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Gültig von</th>
|
||||||
|
<th>Gültig bis</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Ressourcen</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`;
|
||||||
|
|
||||||
|
licenses.forEach(license => {
|
||||||
|
const statusClass = license.status === 'aktiv' ? 'bg-success' :
|
||||||
|
license.status === 'läuft bald ab' ? 'bg-warning' :
|
||||||
|
license.status === 'abgelaufen' ? 'bg-danger' : 'bg-secondary';
|
||||||
|
|
||||||
|
const typeClass = license.license_type === 'full' ? 'bg-primary' : 'bg-secondary';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>${license.license_key}</code>
|
||||||
|
<button class="btn btn-sm btn-link" onclick="copyToClipboard('${license.license_key}')">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge ${typeClass}">${license.license_type.toUpperCase()}</span></td>
|
||||||
|
<td>${license.valid_from || '-'}</td>
|
||||||
|
<td>${license.valid_until || '-'}</td>
|
||||||
|
<td><span class="badge ${statusClass}">${license.status}</span></td>
|
||||||
|
<td>
|
||||||
|
${license.domain_count > 0 ? '🌐 ' + license.domain_count : ''}
|
||||||
|
${license.ipv4_count > 0 ? '📡 ' + license.ipv4_count : ''}
|
||||||
|
${license.phone_count > 0 ? '📱 ' + license.phone_count : ''}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})">
|
||||||
|
<i class="bi bi-power"></i>
|
||||||
|
</button>
|
||||||
|
<a href="/license/edit/${license.id}${window.location.search ? '?ref=customers-licenses&' + window.location.search.substring(1) : ''}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Lizenzstatus
|
||||||
|
function toggleLicenseStatus(licenseId, currentStatus) {
|
||||||
|
const newStatus = !currentStatus;
|
||||||
|
|
||||||
|
fetch(`/api/license/${licenseId}/toggle`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ is_active: newStatus })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Reload current customer licenses
|
||||||
|
if (currentCustomerId) {
|
||||||
|
loadCustomerLicenses(currentCustomerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direkt zur Lizenzerstellung navigieren
|
||||||
|
function showNewLicenseModal(customerId) {
|
||||||
|
window.location.href = `/create?customer_id=${customerId}${window.location.search ? '&' + window.location.search.substring(1) : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to clipboard
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
const button = event.currentTarget;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Zeige kurz Feedback
|
||||||
|
button.innerHTML = '<i class="bi bi-check"></i>';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = '<i class="bi bi-clipboard"></i>';
|
||||||
|
}, 1000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Fehler beim Kopieren:', err);
|
||||||
|
alert('Konnte nicht in die Zwischenablage kopieren');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Testkunden
|
||||||
|
function toggleTestCustomers() {
|
||||||
|
const showTest = document.getElementById('showTestCustomers').checked;
|
||||||
|
const currentUrl = new URL(window.location);
|
||||||
|
currentUrl.searchParams.set('show_test', showTest);
|
||||||
|
window.location.href = currentUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.target.id === 'customerSearch') return; // Nicht bei Suche
|
||||||
|
|
||||||
|
const activeItem = document.querySelector('.customer-item.active');
|
||||||
|
if (!activeItem) return;
|
||||||
|
|
||||||
|
let targetItem = null;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
targetItem = activeItem.previousElementSibling;
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
targetItem = activeItem.nextElementSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetItem && targetItem.classList.contains('customer-item')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const customerId = parseInt(targetItem.dataset.customerId);
|
||||||
|
loadCustomerLicenses(customerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
491
v2_adminpanel/templates/customers_licenses_old.html
Normale Datei
491
v2_adminpanel/templates/customers_licenses_old.html
Normale Datei
@@ -0,0 +1,491 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Kunden & Lizenzen - AccountForger Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>Kunden & Lizenzen</h2>
|
||||||
|
<div>
|
||||||
|
<a href="/create" class="btn btn-success">
|
||||||
|
<i class="bi bi-plus-circle"></i> Neue Lizenz
|
||||||
|
</a>
|
||||||
|
<a href="/batch" class="btn btn-primary">
|
||||||
|
<i class="bi bi-stack"></i> Batch-Lizenzen
|
||||||
|
</a>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-download"></i> Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=excel"><i class="bi bi-file-earmark-excel"></i> Excel</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=csv"><i class="bi bi-file-earmark-csv"></i> CSV</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-house"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Kundenliste (Links) -->
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-people"></i> Kunden
|
||||||
|
<span class="badge bg-secondary float-end">{{ customers|length if customers else 0 }}</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<!-- Suchfeld -->
|
||||||
|
<div class="p-3 border-bottom">
|
||||||
|
<input type="text" class="form-control mb-2" id="customerSearch"
|
||||||
|
placeholder="Kunde suchen..." autocomplete="off">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="showTestCustomers"
|
||||||
|
{% if request.args.get('show_test', 'false').lower() == 'true' %}checked{% endif %}
|
||||||
|
onchange="toggleTestCustomers()">
|
||||||
|
<label class="form-check-label" for="showTestCustomers">
|
||||||
|
<small class="text-muted">Testkunden anzeigen</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kundenliste -->
|
||||||
|
<div class="customer-list" style="max-height: 600px; overflow-y: auto;">
|
||||||
|
{% if customers %}
|
||||||
|
{% for customer in customers %}
|
||||||
|
<div class="customer-item p-3 border-bottom {% if customer[0] == selected_customer_id %}active{% endif %}"
|
||||||
|
data-customer-id="{{ customer[0] }}"
|
||||||
|
data-customer-name="{{ customer[1]|lower }}"
|
||||||
|
data-customer-email="{{ customer[2]|lower }}"
|
||||||
|
onclick="loadCustomerLicenses({{ customer[0] }})"
|
||||||
|
style="cursor: pointer;">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">{{ customer[1] }}</h6>
|
||||||
|
<small class="text-muted">{{ customer[2] }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-primary">{{ customer[4] }}</span>
|
||||||
|
{% if customer[5] > 0 %}
|
||||||
|
<span class="badge bg-success">{{ customer[5] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if customer[6] > 0 %}
|
||||||
|
<span class="badge bg-danger">{{ customer[6] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
<p class="mt-3 mb-2">Keine Kunden vorhanden</p>
|
||||||
|
<small class="d-block mb-3">Erstellen Sie eine neue Lizenz, um automatisch einen Kunden anzulegen.</small>
|
||||||
|
<a href="/create" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Neue Lizenz erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lizenzdetails (Rechts) -->
|
||||||
|
<div class="col-md-8 col-lg-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
{% if selected_customer %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">{{ selected_customer[1] }}</h5>
|
||||||
|
<small class="text-muted">{{ selected_customer[2] }}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/customer/edit/{{ selected_customer[0] }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
|
||||||
|
<i class="bi bi-plus"></i> Neue Lizenz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h5 class="mb-0">Wählen Sie einen Kunden aus</h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="licenseContainer">
|
||||||
|
{% if selected_customer %}
|
||||||
|
{% if licenses %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Lizenzschlüssel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Gültig von</th>
|
||||||
|
<th>Gültig bis</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Ressourcen</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for license in licenses %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>{{ license[1] }}</code>
|
||||||
|
<button class="btn btn-sm btn-link" onclick="copyToClipboard('{{ license[1] }}')">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if license[2] == 'full' %}bg-primary{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ license[2]|upper }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ license[3].strftime('%d.%m.%Y') if license[3] else '-' }}</td>
|
||||||
|
<td>{{ license[4].strftime('%d.%m.%Y') if license[4] else '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge
|
||||||
|
{% if license[6] == 'aktiv' %}bg-success
|
||||||
|
{% elif license[6] == 'läuft bald ab' %}bg-warning
|
||||||
|
{% elif license[6] == 'abgelaufen' %}bg-danger
|
||||||
|
{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ license[6] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if license[7] > 0 %}🌐 {{ license[7] }}{% endif %}
|
||||||
|
{% if license[8] > 0 %}📡 {{ license[8] }}{% endif %}
|
||||||
|
{% if license[9] > 0 %}📱 {{ license[9] }}{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus({{ license[0] }}, {{ license[5] }})">
|
||||||
|
<i class="bi bi-power"></i>
|
||||||
|
</button>
|
||||||
|
<a href="/license/edit/{{ license[0] }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
|
||||||
|
<button class="btn btn-success" onclick="showNewLicenseModal({{ selected_customer[0] }})">
|
||||||
|
<i class="bi bi-plus"></i> Erste Lizenz erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-arrow-left text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">Wählen Sie einen Kunden aus der Liste aus</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal für neue Lizenz -->
|
||||||
|
<div class="modal fade" id="newLicenseModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Neue Lizenz erstellen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Möchten Sie eine neue Lizenz für <strong id="modalCustomerName"></strong> erstellen?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="button" class="btn btn-success" id="createLicenseBtn">Zur Lizenzerstellung</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.customer-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.customer-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left-color: #dee2e6;
|
||||||
|
}
|
||||||
|
.customer-item.active {
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
border-left-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||||
|
}
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Globale Variablen und Funktionen
|
||||||
|
let currentCustomerId = {{ selected_customer_id or 'null' }};
|
||||||
|
|
||||||
|
// Lade Lizenzen eines Kunden
|
||||||
|
function loadCustomerLicenses(customerId) {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
const customerItems = document.querySelectorAll('.customer-item');
|
||||||
|
|
||||||
|
customerItems.forEach(item => {
|
||||||
|
const name = item.dataset.customerName;
|
||||||
|
const email = item.dataset.customerEmail;
|
||||||
|
if (name.includes(searchTerm) || email.includes(searchTerm)) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Aktiven Status aktualisieren
|
||||||
|
document.querySelectorAll('.customer-item').forEach(item => {
|
||||||
|
item.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.querySelector(`[data-customer-id="${customerId}"]`).classList.add('active');
|
||||||
|
|
||||||
|
// URL aktualisieren ohne Reload (behalte show_test Parameter)
|
||||||
|
const currentUrl = new URL(window.location);
|
||||||
|
currentUrl.searchParams.set('customer_id', customerId);
|
||||||
|
window.history.pushState({}, '', currentUrl.toString());
|
||||||
|
|
||||||
|
// Lade Lizenzen via AJAX
|
||||||
|
const container = document.getElementById('licenseContainer');
|
||||||
|
const cardHeader = document.querySelector('.card-header.bg-light');
|
||||||
|
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
|
||||||
|
|
||||||
|
fetch(`/api/customer/${customerId}/licenses`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Update header with customer info
|
||||||
|
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
|
||||||
|
const customerName = customerItem.querySelector('h6').textContent;
|
||||||
|
const customerEmail = customerItem.querySelector('small').textContent;
|
||||||
|
|
||||||
|
cardHeader.innerHTML = `
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">${customerName}</h5>
|
||||||
|
<small class="text-muted">${customerEmail}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/customer/edit/${customerId}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="showNewLicenseModal(${customerId})">
|
||||||
|
<i class="bi bi-plus"></i> Neue Lizenz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
updateLicenseView(customerId, data.licenses);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
container.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Lizenzen</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Lizenzansicht
|
||||||
|
function updateLicenseView(customerId, licenses) {
|
||||||
|
currentCustomerId = customerId;
|
||||||
|
const container = document.getElementById('licenseContainer');
|
||||||
|
|
||||||
|
if (licenses.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">Keine Lizenzen für diesen Kunden vorhanden</p>
|
||||||
|
<button class="btn btn-success" onclick="showNewLicenseModal(${customerId})">
|
||||||
|
<i class="bi bi-plus"></i> Erste Lizenz erstellen
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Lizenzschlüssel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Gültig von</th>
|
||||||
|
<th>Gültig bis</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Ressourcen</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`;
|
||||||
|
|
||||||
|
licenses.forEach(license => {
|
||||||
|
const statusClass = license.status === 'aktiv' ? 'bg-success' :
|
||||||
|
license.status === 'läuft bald ab' ? 'bg-warning' :
|
||||||
|
license.status === 'abgelaufen' ? 'bg-danger' : 'bg-secondary';
|
||||||
|
|
||||||
|
const typeClass = license.license_type === 'full' ? 'bg-primary' : 'bg-secondary';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>${license.license_key}</code>
|
||||||
|
<button class="btn btn-sm btn-link" onclick="copyToClipboard('${license.license_key}')">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge ${typeClass}">${license.license_type.toUpperCase()}</span></td>
|
||||||
|
<td>${license.valid_from || '-'}</td>
|
||||||
|
<td>${license.valid_until || '-'}</td>
|
||||||
|
<td><span class="badge ${statusClass}">${license.status}</span></td>
|
||||||
|
<td>
|
||||||
|
${license.domain_count > 0 ? '🌐 ' + license.domain_count : ''}
|
||||||
|
${license.ipv4_count > 0 ? '📡 ' + license.ipv4_count : ''}
|
||||||
|
${license.phone_count > 0 ? '📱 ' + license.phone_count : ''}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})">
|
||||||
|
<i class="bi bi-power"></i>
|
||||||
|
</button>
|
||||||
|
<a href="/license/edit/${license.id}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Lizenzstatus
|
||||||
|
function toggleLicenseStatus(licenseId, currentStatus) {
|
||||||
|
const newStatus = !currentStatus;
|
||||||
|
|
||||||
|
fetch(`/api/license/${licenseId}/toggle`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ is_active: newStatus })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Reload current customer licenses
|
||||||
|
if (currentCustomerId) {
|
||||||
|
loadCustomerLicenses(currentCustomerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige Modal für neue Lizenz
|
||||||
|
function showNewLicenseModal(customerId) {
|
||||||
|
const customerItem = document.querySelector(`[data-customer-id="${customerId}"]`);
|
||||||
|
if (!customerItem) {
|
||||||
|
console.error('Kunde nicht gefunden:', customerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerName = customerItem.querySelector('h6').textContent;
|
||||||
|
|
||||||
|
document.getElementById('modalCustomerName').textContent = customerName;
|
||||||
|
document.getElementById('createLicenseBtn').onclick = function() {
|
||||||
|
window.location.href = `/create?customer_id=${customerId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if bootstrap is loaded
|
||||||
|
if (typeof bootstrap === 'undefined') {
|
||||||
|
console.error('Bootstrap nicht geladen!');
|
||||||
|
// Fallback: Direkt zur Erstellung
|
||||||
|
if (confirm(`Neue Lizenz für ${customerName} erstellen?`)) {
|
||||||
|
window.location.href = `/create?customer_id=${customerId}`;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalElement = document.getElementById('newLicenseModal');
|
||||||
|
const modal = new bootstrap.Modal(modalElement);
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to clipboard
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
const button = event.currentTarget;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Zeige kurz Feedback
|
||||||
|
button.innerHTML = '<i class="bi bi-check"></i>';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = '<i class="bi bi-clipboard"></i>';
|
||||||
|
}, 1000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Fehler beim Kopieren:', err);
|
||||||
|
alert('Konnte nicht in die Zwischenablage kopieren');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Testkunden
|
||||||
|
function toggleTestCustomers() {
|
||||||
|
const showTest = document.getElementById('showTestCustomers').checked;
|
||||||
|
const currentUrl = new URL(window.location);
|
||||||
|
currentUrl.searchParams.set('show_test', showTest);
|
||||||
|
window.location.href = currentUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.target.id === 'customerSearch') return; // Nicht bei Suche
|
||||||
|
|
||||||
|
const activeItem = document.querySelector('.customer-item.active');
|
||||||
|
if (!activeItem) return;
|
||||||
|
|
||||||
|
let targetItem = null;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
targetItem = activeItem.previousElementSibling;
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
targetItem = activeItem.nextElementSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetItem && targetItem.classList.contains('customer-item')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const customerId = parseInt(targetItem.dataset.customerId);
|
||||||
|
loadCustomerLicenses(customerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}); // Ende DOMContentLoaded
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<div>
|
<div>
|
||||||
|
<a href="/customers-licenses" class="btn btn-success">👥 Kunden & Lizenzen</a>
|
||||||
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
||||||
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
||||||
<a href="/audit" class="btn btn-secondary">📝 Log</a>
|
<a href="/audit" class="btn btn-secondary">📝 Log</a>
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
<!-- Statistik-Karten -->
|
<!-- Statistik-Karten -->
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<a href="/customers" class="text-decoration-none">
|
<a href="/customers-licenses" class="text-decoration-none">
|
||||||
<div class="card stat-card h-100">
|
<div class="card stat-card h-100">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<div class="card-icon text-primary">👥</div>
|
<div class="card-icon text-primary">👥</div>
|
||||||
@@ -88,7 +89,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<a href="/licenses" class="text-decoration-none">
|
<a href="/customers-licenses" class="text-decoration-none">
|
||||||
<div class="card stat-card h-100">
|
<div class="card stat-card h-100">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<div class="card-icon text-info">📋</div>
|
<div class="card-icon text-info">📋</div>
|
||||||
|
|||||||
@@ -8,13 +8,16 @@
|
|||||||
<h2>Kunde bearbeiten</h2>
|
<h2>Kunde bearbeiten</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
<a href="/customers" class="btn btn-secondary">👥 Zurück zur Übersicht</a>
|
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">👥 Zurück zur Übersicht</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="/customer/edit/{{ customer[0] }}" accept-charset="UTF-8">
|
<form method="post" action="/customer/edit/{{ customer[0] }}" accept-charset="UTF-8">
|
||||||
|
{% if request.args.get('show_test') == 'true' %}
|
||||||
|
<input type="hidden" name="show_test" value="true">
|
||||||
|
{% endif %}
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="name" class="form-label">Kundenname</label>
|
<label for="name" class="form-label">Kundenname</label>
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mt-3">
|
<div class="form-check mt-3">
|
||||||
<input class="form-check-input" type="checkbox" id="isTest" name="is_test" {% if customer[3] %}checked{% endif %}>
|
<input class="form-check-input" type="checkbox" id="isTest" name="is_test" {% if customer[4] %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="isTest">
|
<label class="form-check-label" for="isTest">
|
||||||
<i class="fas fa-flask"></i> Als Testdaten markieren
|
<i class="fas fa-flask"></i> Als Testdaten markieren
|
||||||
<small class="text-muted">(Kunde und seine Lizenzen werden von der Software ignoriert)</small>
|
<small class="text-muted">(Kunde und seine Lizenzen werden von der Software ignoriert)</small>
|
||||||
@@ -40,7 +43,7 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
||||||
<a href="/customers" class="btn btn-secondary">Abbrechen</a>
|
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">Abbrechen</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,13 +8,16 @@
|
|||||||
<h2>Lizenz bearbeiten</h2>
|
<h2>Lizenz bearbeiten</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
<a href="/licenses" class="btn btn-secondary">📋 Zurück zur Übersicht</a>
|
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">📋 Zurück zur Übersicht</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="/license/edit/{{ license[0] }}" accept-charset="UTF-8">
|
<form method="post" action="/license/edit/{{ license[0] }}" accept-charset="UTF-8">
|
||||||
|
{% if request.args.get('show_test') == 'true' %}
|
||||||
|
<input type="hidden" name="show_test" value="true">
|
||||||
|
{% endif %}
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Kunde</label>
|
<label class="form-label">Kunde</label>
|
||||||
@@ -64,7 +67,7 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
||||||
<a href="/licenses" class="btn btn-secondary">Abbrechen</a>
|
<a href="/customers-licenses{% if request.args.get('show_test') == 'true' %}?show_test=true{% endif %}" class="btn btn-secondary">Abbrechen</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
// Event Handler für Kundenauswahl
|
||||||
$('#customerSelect').on('select2:select', function (e) {
|
$('#customerSelect').on('select2:select', function (e) {
|
||||||
const selectedValue = e.params.data.id;
|
const selectedValue = e.params.data.id;
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
<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>Lizenzübersicht</h2>
|
<h2>Lizenzübersicht</h2>
|
||||||
<div>
|
<div>
|
||||||
|
<a href="/customers-licenses" class="btn btn-success">
|
||||||
|
<i class="bi bi-layout-split"></i> Kombinierte Ansicht
|
||||||
|
</a>
|
||||||
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
|
||||||
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
||||||
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren