From 01175662681a6a137713cdfacbb35ff36a297548 Mon Sep 17 00:00:00 2001 From: "XPS\\Micro" Date: Sun, 8 Feb 2026 16:56:04 +0100 Subject: [PATCH] feat: Expandable Container-Rows + shadcn AlertDialog + Status 207 Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: Status 207 → 200 für partielle Erfolge (admin_api.py) - Frontend: Expandable User-Rows mit Container-Checkboxen (admin/page.tsx) - UI: Neues shadcn AlertDialog für Container-Lösch-Bestätigung - Deps: @radix-ui/react-alert-dialog installiert - Docs: Version 3.0 Dokumentation aktualisiert (admin-dashboard-improvements.md) Behebt: - Problem I: Browser-confirm() → echtes Modal - Problem II: Status 207 Fehler (0 gelöscht, 1 fehlgeschlagen) --- admin_api.py | 21 +- docs/guides/admin-dashboard-improvements.md | 81 +++++- frontend/package.json | 19 +- frontend/src/app/admin/page.tsx | 298 +++++++++++++++----- frontend/src/components/ui/alert-dialog.tsx | 132 +++++++++ 5 files changed, 452 insertions(+), 99 deletions(-) create mode 100644 frontend/src/components/ui/alert-dialog.tsx diff --git a/admin_api.py b/admin_api.py index 34b3de2..fa94d1d 100644 --- a/admin_api.py +++ b/admin_api.py @@ -198,7 +198,12 @@ def delete_user_container(user_id): return jsonify({'error': 'User nicht gefunden'}), 404 if not user.containers: - return jsonify({'error': 'User hat keine Container'}), 400 + return jsonify({ + 'message': 'User hat keine Container', + 'deleted': 0, + 'failed': [], + 'skipped': True + }), 200 container_mgr = ContainerManager() deleted_count = 0 @@ -225,16 +230,12 @@ def delete_user_container(user_id): current_app.logger.info(f"Admin {admin_id} löschte {deleted_count} Container von User {user.email}") - if failed_containers: - return jsonify({ - 'message': f'{deleted_count} Container gelöscht, {len(failed_containers)} fehlgeschlagen', - 'failed': failed_containers, - 'deleted': deleted_count - }), 207 # Multi-Status - return jsonify({ - 'message': f'Alle {deleted_count} Container von {user.email} wurden gelöscht', - 'deleted': deleted_count + 'message': f'{deleted_count} Container gelöscht' + + (f', {len(failed_containers)} fehlgeschlagen' if failed_containers else ''), + 'deleted': deleted_count, + 'failed': failed_containers, + 'partial_failure': len(failed_containers) > 0 }), 200 diff --git a/docs/guides/admin-dashboard-improvements.md b/docs/guides/admin-dashboard-improvements.md index e892e8e..1aea40a 100644 --- a/docs/guides/admin-dashboard-improvements.md +++ b/docs/guides/admin-dashboard-improvements.md @@ -1,7 +1,7 @@ # Admin-Dashboard: Verbesserte Container- und User-Löschung -**Datum:** 02.02.2026 -**Version:** 2.0 +**Datum:** 08.02.2026 (Update), 02.02.2026 (Initial) +**Version:** 3.0 **Status:** ✅ Vollständig implementiert --- @@ -14,6 +14,82 @@ Diese Dokumentation beschreibt die Verbesserungen des Admin-Dashboards: 2. **Toast-Benachrichtigungen** - Modernes UI statt primitiver Alerts 3. **Bulk-Operations** - Mehrere User gleichzeitig verwalten (Sperren, Löschen, etc.) 4. **DSGVO-Compliance** - Vollständige Datenlöschung (MagicLinkToken, AdminTakeoverSession) +5. **✨ NEU (v3.0)**: Expandable Container-Rows + shadcn AlertDialog (08.02.2026) + +--- + +## ✨ Version 3.0: Expandable Rows & AlertDialog (08.02.2026) + +### 🎯 Problem (Gelöst) + +**Problem I:** Browser-`confirm()` statt echtes Modal +- Admin konnte nicht einzelne Container auswählen +- Nur "Alles löschen oder gar nichts" möglich +- Keine Übersicht der betroffenen Container + +**Problem II:** Toast zeigt "0 gelöscht, 1 fehlgeschlagen" +- **Root Cause**: Backend gab Status 207 zurück +- Frontend interpretierte Status 207 als Fehler → `response.ok = false` +- Obwohl Container erfolgreich gelöscht wurden, zählte Frontend sie als "fehlgeschlagen" + +### ✅ Lösung Implementiert + +#### Backend-Fix (`admin_api.py` Zeile 189-238) +- Status 207 → **200** (für partielle Erfolge) +- Status 400 → **200** (für "keine Container") +- Details im Response-Body: `deleted`, `failed`, `partial_failure` +- **Begründung**: HTTP-Status sollten Transport-Errors signalisieren, nicht Business-Logic + +```python +# VORHER: Status 207 für Teilerfolge +return jsonify({...}), 207 + +# NACHHER: Immer Status 200, Details im Body +return jsonify({ + 'message': f'{deleted_count} Container gelöscht' + + (f', {len(failed_containers)} fehlgeschlagen' if failed_containers else ''), + 'deleted': deleted_count, + 'failed': failed_containers, + 'partial_failure': len(failed_containers) > 0 +}), 200 +``` + +#### Frontend-Verbesserungen (`frontend/src/app/admin/page.tsx`) + +**Expandable User-Rows:** +- Klick auf User → Container-Liste klappt auf/zu +- ChevronDown Icon rotiert bei Expand/Collapse +- Container nur angezeigt wenn User expandiert + +**Container-Checkboxen:** +- Jeder Container hat eine Checkbox +- Nur ausgewählte Container werden gelöscht +- Unterstützung für Einzelaus wahl und Bulk-Select + +**shadcn AlertDialog (neu):** +- Echtes Modal statt Browser-`confirm()` +- Zeigt Zusammenfassung: + ``` + 3 Container von 2 Benutzer(n) löschen? + • user1@example.com (2 Container) + • user2@example.com (1 Container) + ``` +- Ja/Nein Buttons mit klarer Bestätigung + +**Verbesserte Bulk-Delete-Logik:** +- Parst `deleted` und `failed` aus Response-Body +- Akkumuliert Container-Zähler (nicht User-Zähler) +- Toast zeigt genaue Zahlen: "3 Container gelöscht, 1 fehlgeschlagen" + +### 📝 Dateiänderungen + +``` +✅ Geänderte Dateien: +- admin_api.py (Backend-Fix für Status 207) +- frontend/src/app/admin/page.tsx (UI-Overhaul) +- frontend/src/components/ui/alert-dialog.tsx (neue Komponente) +- frontend/package.json (@radix-ui/react-alert-dialog) +``` --- @@ -410,6 +486,7 @@ with app.app_context(): | Version | Datum | Änderungen | |---------|-------|-----------| +| 3.0 | 08.02.2026 | **Expandable Rows**, shadcn AlertDialog, Status 207→200 Fix, Container-Checkboxen | | 2.0 | 02.02.2026 | Multi-Container, Toast-System, Bulk-Operations, DSGVO | | 1.0 | ≤01.02.2026 | Ursprüngliches Admin-Dashboard | diff --git a/frontend/package.json b/frontend/package.json index b0c1952..e5370d6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,19 +9,20 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.408.0", "next": "14.2.5", "react": "^18.3.1", "react-dom": "^18.3.1", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "tailwind-merge": "^2.4.0", - "lucide-react": "^0.408.0", "sonner": "1.7.2", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-avatar": "^1.1.0", - "@radix-ui/react-dropdown-menu": "^2.1.1" + "tailwind-merge": "^2.4.0" }, "devDependencies": { "@types/node": "^20.14.10", diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index e8d1083..571b14b 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -34,8 +34,19 @@ import { Search, Monitor, X, + ChevronDown, } from "lucide-react"; import { toast } from "sonner"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; type StatusColor = "green" | "yellow" | "red"; @@ -114,6 +125,12 @@ export default function AdminPage() { const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); const [activeTab, setActiveTab] = useState<"users" | "containers">("users"); const [selectedContainerIds, setSelectedContainerIds] = useState>(new Set()); + const [expandedUserIds, setExpandedUserIds] = useState>(new Set()); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [deleteDialogData, setDeleteDialogData] = useState<{ + containerIds: number[]; + userSummary: { email: string; count: number }[]; + } | null>(null); const fetchUsers = useCallback(async () => { setIsLoading(true); @@ -130,6 +147,96 @@ export default function AdminPage() { fetchUsers(); }, [fetchUsers]); + // Expand/Collapse Helper + const toggleUserExpand = (userId: number) => { + const newExpanded = new Set(expandedUserIds); + if (newExpanded.has(userId)) { + newExpanded.delete(userId); + } else { + newExpanded.add(userId); + } + setExpandedUserIds(newExpanded); + }; + + // Dialog Helper für Bulk-Delete + const openBulkDeleteDialog = () => { + if (selectedContainerIds.size === 0) { + toast.error("Keine Container ausgewählt"); + return; + } + + // Erstelle Zusammenfassung nach User + const userMap = new Map(); + + for (const containerId of selectedContainerIds) { + const user = users.find(u => + u.containers?.some(c => c.id === containerId) + ); + if (user) { + const existing = userMap.get(user.id) || { email: user.email, count: 0 }; + existing.count++; + userMap.set(user.id, existing); + } + } + + setDeleteDialogData({ + containerIds: Array.from(selectedContainerIds), + userSummary: Array.from(userMap.values()) + }); + setIsDeleteDialogOpen(true); + }; + + // Bestätigte Bulk-Delete + const handleConfirmBulkDelete = async () => { + if (!deleteDialogData) return; + + setIsDeleteDialogOpen(false); + toast.loading( + `Lösche ${deleteDialogData.containerIds.length} Container...`, + { id: "bulk-delete-containers" } + ); + + // Gruppiere Container nach User-ID + const containersByUser = new Map(); + + for (const containerId of deleteDialogData.containerIds) { + const user = users.find(u => + u.containers?.some(c => c.id === containerId) + ); + if (user) { + if (!containersByUser.has(user.id)) { + containersByUser.set(user.id, []); + } + containersByUser.get(user.id)!.push(containerId); + } + } + + let totalDeleted = 0; + let totalFailed = 0; + + // Lösche Container pro User + for (const [userId, containerIds] of containersByUser) { + const { data, error } = await adminApi.deleteUserContainer(userId); + + if (error) { + totalFailed += containerIds.length; + } else if (data) { + // Parse Response-Body (nach Backend-Fix) + totalDeleted += data.deleted || 0; + totalFailed += (data.failed?.length || 0); + } + } + + toast.success(`${totalDeleted} Container gelöscht`, { + id: "bulk-delete-containers", + description: totalFailed > 0 ? `${totalFailed} fehlgeschlagen` : undefined, + }); + + fetchUsers(); + setSelectedContainerIds(new Set()); + setDeleteDialogData(null); + }; + // Bulk-Selection Helpers const toggleUserSelection = (userId: number) => { const newSelection = new Set(selectedUserIds); @@ -188,23 +295,6 @@ export default function AdminPage() { setActionLoading(null); }; - const handleDeleteContainer = async (userId: number, userEmail: string) => { - if (!confirm(`Container von "${userEmail}" wirklich loeschen? Der User kann einen neuen Container starten.`)) { - return; - } - setActionLoading(userId); - const { data, error } = await adminApi.deleteUserContainer(userId); - if (error) { - toast.error(`Fehler: ${error}`); - } else { - toast.success(data?.message || "Container geloescht", { - description: data?.deleted ? `${data.deleted} Container entfernt` : undefined, - }); - fetchUsers(); - } - setActionLoading(null); - }; - const handleDeleteUser = async (userId: number, userEmail: string) => { if (!confirm( `⚠️ ACHTUNG: User "${userEmail}" VOLLSTAENDIG loeschen?\n\n` + @@ -304,43 +394,6 @@ export default function AdminPage() { deselectAll(); }; - const handleBulkDeleteContainers = async () => { - const userList = Array.from(selectedUserIds) - .map((id) => users.find((u) => u.id === id)?.email) - .filter(Boolean) - .join(", "); - - if (!confirm( - `Container von ${selectedUserIds.size} Usern löschen?\n\n` + - `Betroffene User:\n${userList}\n\n` + - `User können danach neue Container erstellen.` - )) { - return; - } - - toast.loading(`Lösche Container von ${selectedUserIds.size} Usern...`, { id: "bulk-delete-containers" }); - - let success = 0; - let failed = 0; - - for (const userId of selectedUserIds) { - const { error } = await adminApi.deleteUserContainer(userId); - if (error) { - failed++; - } else { - success++; - } - } - - toast.success(`${success} User-Container gelöscht`, { - id: "bulk-delete-containers", - description: failed > 0 ? `${failed} fehlgeschlagen` : undefined, - }); - - fetchUsers(); - deselectAll(); - }; - const handleBulkDeleteUsers = async () => { const selectedUsers = Array.from(selectedUserIds) .map((id) => users.find((u) => u.id === id)) @@ -717,15 +770,17 @@ export default function AdminPage() { {/* Bulk-Delete-Container */} - + {selectedContainerIds.size > 0 && ( + + )} {/* Bulk-Delete User */} + )} + {isSelectable && ( - {/* Container loeschen */} - {u.container_id && ( - - )} - {/* Takeover (Dummy) */} {u.container_id && !isCurrentUser && (