feat(admin): add user data dashboard for cross-project data visualization

Add comprehensive admin dashboard to view and manage user data across all projects:

Backend:
- Add admin endpoints to Chat, Todo, Contacts, Calendar, Picture, Zitare, Presi
- Each backend exposes GET/DELETE /api/v1/admin/user-data/:userId
- Service-to-service auth via X-Service-Key header

Aggregation (mana-core-auth):
- GET /api/v1/admin/users - Paginated user list with search
- GET /api/v1/admin/users/:userId/data - Aggregated data from all backends
- DELETE /api/v1/admin/users/:userId/data - GDPR deletion across all projects

Frontend (ManaCore web):
- New User Data tab in admin navigation
- User search page at /admin/user-data
- User detail page with ProjectDataCard components
- GDPR deletion dialog with email confirmation

Presi:
- Migrate user_id from UUID to TEXT for Better Auth compatibility
- Add SQL migration script
This commit is contained in:
Till-JS 2026-02-11 14:59:18 +01:00
parent 5b6f231e1a
commit a2e2a5b73c
57 changed files with 3847 additions and 465 deletions

View file

@ -0,0 +1,185 @@
/**
* Admin API Service
*
* Provides admin functionality for managing user data across all projects.
*/
import { browser } from '$app/environment';
import { createApiClient, type ApiResult } from '../base-client';
// Get Auth API URL dynamically at runtime (admin endpoints are on auth service)
function getAuthApiUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
if (injectedUrl) {
return `${injectedUrl}/api/v1`;
}
}
return 'http://localhost:3001/api/v1';
}
// Lazy-initialized client
let _client: ReturnType<typeof createApiClient> | null = null;
function getClient() {
if (!_client) {
_client = createApiClient(getAuthApiUrl());
}
return _client;
}
/**
* User list item from admin API
*/
export interface UserListItem {
id: string;
email: string;
name: string;
role: string;
createdAt: string;
lastActiveAt?: string;
}
/**
* User list response with pagination
*/
export interface UserListResponse {
users: UserListItem[];
total: number;
page: number;
limit: number;
}
/**
* Entity count for a project
*/
export interface EntityCount {
entity: string;
count: number;
label: string;
}
/**
* Project data summary
*/
export interface ProjectDataSummary {
projectId: string;
projectName: string;
icon: string;
available: boolean;
error?: string;
entities: EntityCount[];
totalCount: number;
lastActivityAt?: string;
}
/**
* User info
*/
export interface UserInfo {
id: string;
email: string;
name: string;
role: string;
createdAt: string;
emailVerified: boolean;
}
/**
* Auth data summary
*/
export interface AuthDataSummary {
sessionsCount: number;
accountsCount: number;
has2FA: boolean;
lastLoginAt: string | null;
}
/**
* Credits data summary
*/
export interface CreditsDataSummary {
balance: number;
totalEarned: number;
totalSpent: number;
transactionsCount: number;
}
/**
* Full user data summary
*/
export interface UserDataSummary {
user: UserInfo;
auth: AuthDataSummary;
credits: CreditsDataSummary;
projects: ProjectDataSummary[];
totals: {
totalEntities: number;
projectsWithData: number;
};
}
/**
* Delete result for a project
*/
export interface ProjectDeleteResult {
projectId: string;
projectName: string;
success: boolean;
deletedCount?: number;
error?: string;
}
/**
* Full delete response
*/
export interface DeleteUserDataResponse {
success: boolean;
deletedFromProjects: ProjectDeleteResult[];
deletedFromAuth: {
sessions: number;
accounts: number;
credits: number;
user: boolean;
};
totalDeleted: number;
}
/**
* Admin service for user data management
*/
export const adminService = {
/**
* Get list of users with pagination and search
*/
async getUsers(
page: number = 1,
limit: number = 20,
search?: string
): Promise<ApiResult<UserListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (search) {
params.set('search', search);
}
return getClient().get<UserListResponse>(`/admin/users?${params.toString()}`);
},
/**
* Get aggregated user data from all projects
*/
async getUserData(userId: string): Promise<ApiResult<UserDataSummary>> {
return getClient().get<UserDataSummary>(`/admin/users/${userId}/data`);
},
/**
* Delete all user data (GDPR right to be forgotten)
*/
async deleteUserData(userId: string): Promise<ApiResult<DeleteUserDataResponse>> {
return getClient().delete<DeleteUserDataResponse>(`/admin/users/${userId}/data`);
},
};

View file

@ -0,0 +1,83 @@
<script lang="ts">
import type { ProjectDataSummary } from '$lib/api/services/admin';
interface Props {
project: ProjectDataSummary;
}
let { project }: Props = $props();
function formatRelativeTime(dateStr: string | undefined): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'gerade eben';
if (diffMins < 60) return `vor ${diffMins} Min`;
if (diffHours < 24) return `vor ${diffHours} Std`;
if (diffDays < 7) return `vor ${diffDays} Tagen`;
return new Date(dateStr).toLocaleDateString('de-DE');
}
</script>
<div
class="rounded-lg border bg-card shadow-sm overflow-hidden {project.available
? ''
: 'opacity-60'}"
>
<div class="p-4 border-b flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-2xl">{project.icon}</span>
<div>
<h3 class="font-semibold">{project.projectName}</h3>
{#if project.available}
<p class="text-xs text-muted-foreground">
{project.totalCount} Einträge
</p>
{:else}
<p class="text-xs text-red-500">{project.error || 'Nicht verfügbar'}</p>
{/if}
</div>
</div>
{#if project.available}
<div
class="h-2 w-2 rounded-full {project.totalCount > 0 ? 'bg-green-500' : 'bg-gray-300'}"
></div>
{:else}
<div class="h-2 w-2 rounded-full bg-red-500"></div>
{/if}
</div>
{#if project.available}
<div class="p-4">
{#if project.entities.length > 0}
<div class="space-y-2">
{#each project.entities as entity}
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{entity.label}</span>
<span class="font-mono font-medium">{entity.count}</span>
</div>
{/each}
</div>
{#if project.lastActivityAt}
<div class="mt-4 pt-3 border-t">
<p class="text-xs text-muted-foreground">
Letzte Aktivitat: {formatRelativeTime(project.lastActivityAt)}
</p>
</div>
{/if}
{:else}
<p class="text-sm text-muted-foreground text-center py-4">Keine Daten vorhanden</p>
{/if}
</div>
{:else}
<div class="p-4">
<p class="text-sm text-muted-foreground text-center py-2">Backend nicht erreichbar</p>
</div>
{/if}
</div>

View file

@ -7,12 +7,14 @@
const tabs = [
{ href: '/admin', label: 'Overview', icon: 'home' },
{ href: '/admin/users', label: 'Users', icon: 'users' },
{ href: '/admin/user-data', label: 'User Data', icon: 'database' },
{ href: '/admin/system', label: 'System', icon: 'server' },
];
const icons: Record<string, string> = {
home: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />`,
users: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />`,
database: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />`,
server: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />`,
};

View file

@ -0,0 +1,254 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { adminService, type UserListItem } from '$lib/api/services/admin';
let users = $state<UserListItem[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
let searchQuery = $state('');
let searchTimeout: ReturnType<typeof setTimeout>;
let page = $state(1);
let total = $state(0);
const limit = 20;
let totalPages = $derived(Math.ceil(total / limit));
async function loadUsers() {
loading = true;
error = null;
const result = await adminService.getUsers(page, limit, searchQuery || undefined);
if (result.error) {
error = result.error;
users = [];
} else if (result.data) {
users = result.data.users;
total = result.data.total;
}
loading = false;
}
function handleSearch() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
page = 1;
loadUsers();
}, 300);
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
function formatRelativeTime(dateStr: string | undefined): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'gerade eben';
if (diffMins < 60) return `vor ${diffMins} Min`;
if (diffHours < 24) return `vor ${diffHours} Std`;
if (diffDays < 7) return `vor ${diffDays} Tagen`;
return formatDate(dateStr);
}
function viewUserData(userId: string) {
goto(`/admin/user-data/${userId}`);
}
onMount(() => {
loadUsers();
});
</script>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Nutzerdaten</h1>
<p class="text-muted-foreground">Durchsuche und analysiere Nutzerdaten aller Projekte</p>
</div>
</div>
<!-- Search -->
<div class="flex items-center gap-4">
<div class="relative flex-1 max-w-md">
<svg
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
type="text"
placeholder="Nach Email oder Name suchen..."
bind:value={searchQuery}
oninput={handleSearch}
class="w-full pl-10 pr-4 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<span class="text-sm text-muted-foreground">
{total} Nutzer gefunden
</span>
</div>
<!-- User Table -->
<div class="rounded-lg border bg-card shadow-sm overflow-hidden">
<div class="p-4 border-b">
<h3 class="text-lg font-semibold">Nutzer</h3>
</div>
{#if loading}
<div class="p-4 space-y-3">
{#each Array(5) as _}
<div class="animate-pulse flex items-center gap-4">
<div class="h-10 w-10 bg-muted rounded-full"></div>
<div class="flex-1">
<div class="h-4 bg-muted rounded w-1/4 mb-2"></div>
<div class="h-3 bg-muted rounded w-1/3"></div>
</div>
</div>
{/each}
</div>
{:else if error}
<div class="p-8 text-center">
<p class="text-red-500">{error}</p>
<button
onclick={() => loadUsers()}
class="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Erneut versuchen
</button>
</div>
{:else}
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-muted/50">
<tr>
<th
class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
Nutzer
</th>
<th
class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
Rolle
</th>
<th
class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
Registriert
</th>
<th
class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
Letzte Aktivitat
</th>
<th
class="px-4 py-3 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
Aktionen
</th>
</tr>
</thead>
<tbody class="divide-y">
{#each users as user}
<tr class="hover:bg-muted/30 transition-colors">
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div
class="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center"
>
<span class="text-sm font-medium text-primary">
{(user.name || user.email)[0].toUpperCase()}
</span>
</div>
<div>
<p class="font-medium text-sm">{user.name || '-'}</p>
<p class="text-xs text-muted-foreground">{user.email}</p>
</div>
</div>
</td>
<td class="px-4 py-3">
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
{user.role === 'admin'
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'}"
>
{user.role}
</span>
</td>
<td class="px-4 py-3 text-sm text-muted-foreground">
{formatDate(user.createdAt)}
</td>
<td class="px-4 py-3 text-sm text-muted-foreground">
{formatRelativeTime(user.lastActiveAt)}
</td>
<td class="px-4 py-3 text-right">
<button
onclick={() => viewUserData(user.id)}
class="px-3 py-1.5 text-sm bg-primary/10 text-primary rounded-md hover:bg-primary/20 transition-colors"
>
Daten anzeigen
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if users.length === 0}
<div class="p-8 text-center text-muted-foreground">Keine Nutzer gefunden</div>
{/if}
<!-- Pagination -->
{#if totalPages > 1}
<div class="p-4 border-t flex items-center justify-between">
<button
onclick={() => {
page = Math.max(1, page - 1);
loadUsers();
}}
disabled={page === 1}
class="px-3 py-1.5 text-sm border rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted"
>
Zuruck
</button>
<span class="text-sm text-muted-foreground">
Seite {page} von {totalPages}
</span>
<button
onclick={() => {
page = Math.min(totalPages, page + 1);
loadUsers();
}}
disabled={page === totalPages}
class="px-3 py-1.5 text-sm border rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted"
>
Weiter
</button>
</div>
{/if}
{/if}
</div>
</div>

View file

@ -0,0 +1,401 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import StatCard from '$lib/components/admin/StatCard.svelte';
import ProjectDataCard from '$lib/components/admin/ProjectDataCard.svelte';
import {
adminService,
type UserDataSummary,
type DeleteUserDataResponse,
} from '$lib/api/services/admin';
let userId = $derived($page.params.userId ?? '');
let userData = $state<UserDataSummary | null>(null);
let loading = $state(true);
let error = $state<string | null>(null);
// Delete dialog state
let showDeleteDialog = $state(false);
let deleteConfirmEmail = $state('');
let deleting = $state(false);
let deleteResult = $state<DeleteUserDataResponse | null>(null);
let deleteError = $state<string | null>(null);
async function loadUserData() {
if (!userId) {
error = 'Keine Nutzer-ID angegeben';
loading = false;
return;
}
loading = true;
error = null;
const result = await adminService.getUserData(userId);
if (result.error) {
error = result.error;
userData = null;
} else {
userData = result.data;
}
loading = false;
}
async function handleDelete() {
if (!userData || !userId || deleteConfirmEmail !== userData.user.email) {
return;
}
deleting = true;
deleteError = null;
const result = await adminService.deleteUserData(userId);
if (result.error) {
deleteError = result.error;
} else {
deleteResult = result.data;
}
deleting = false;
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
onMount(() => {
loadUserData();
});
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center gap-4">
<button
onclick={() => goto('/admin/user-data')}
class="p-2 rounded-lg hover:bg-muted transition-colors"
aria-label="Zuruck zur Nutzerliste"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div class="flex-1">
<h1 class="text-2xl font-bold">Nutzerdaten</h1>
<p class="text-muted-foreground">
{userData?.user.email || 'Laden...'}
</p>
</div>
{#if userData}
<button
onclick={() => (showDeleteDialog = true)}
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
Daten loschen
</button>
{/if}
</div>
{#if loading}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{#each Array(4) as _}
<div class="rounded-lg border bg-card p-6 shadow-sm animate-pulse">
<div class="h-4 bg-muted rounded w-20 mb-2"></div>
<div class="h-8 bg-muted rounded w-16"></div>
</div>
{/each}
</div>
{:else if error}
<div
class="rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20 p-6 text-center"
>
<p class="text-red-600 dark:text-red-400 mb-4">{error}</p>
<button
onclick={loadUserData}
class="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Erneut versuchen
</button>
</div>
{:else if userData}
<!-- User Info Card -->
<div class="rounded-lg border bg-card p-6 shadow-sm">
<div class="flex items-start gap-4">
<div class="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center">
<span class="text-2xl font-bold text-primary">
{(userData.user.name || userData.user.email)[0].toUpperCase()}
</span>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold">{userData.user.name || 'Kein Name'}</h2>
<p class="text-muted-foreground">{userData.user.email}</p>
<div class="flex items-center gap-4 mt-2">
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
{userData.user.role === 'admin'
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'}"
>
{userData.user.role}
</span>
{#if userData.user.emailVerified}
<span class="text-xs text-green-600 flex items-center gap-1">
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
Email verifiziert
</span>
{:else}
<span class="text-xs text-yellow-600 flex items-center gap-1">
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
Email nicht verifiziert
</span>
{/if}
</div>
<p class="text-xs text-muted-foreground mt-2">
Registriert am {formatDate(userData.user.createdAt)}
</p>
</div>
</div>
</div>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="Gesamt-Entitaten" value={userData.totals.totalEntities} icon="chart" />
<StatCard
title="Projekte mit Daten"
value="{userData.totals.projectsWithData} / {userData.projects.length}"
icon="activity"
/>
<StatCard title="Credits" value={userData.credits.balance} icon="chart" />
<StatCard title="Transaktionen" value={userData.credits.transactionsCount} icon="clock" />
</div>
<!-- Auth & Credits Details -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Auth Data -->
<div class="rounded-lg border bg-card p-6 shadow-sm">
<h3 class="text-lg font-semibold mb-4">Authentifizierung</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Aktive Sessions</span>
<span class="font-mono">{userData.auth.sessionsCount}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Verknupfte Accounts</span>
<span class="font-mono">{userData.auth.accountsCount}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">2FA aktiviert</span>
<span class={userData.auth.has2FA ? 'text-green-500' : 'text-muted-foreground'}>
{userData.auth.has2FA ? 'Ja' : 'Nein'}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Letzter Login</span>
<span class="text-sm">
{userData.auth.lastLoginAt ? formatDate(userData.auth.lastLoginAt) : '-'}
</span>
</div>
</div>
</div>
<!-- Credits Data -->
<div class="rounded-lg border bg-card p-6 shadow-sm">
<h3 class="text-lg font-semibold mb-4">Credits</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Aktueller Stand</span>
<span class="font-mono font-bold text-lg">{userData.credits.balance}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Gesamt verdient</span>
<span class="font-mono text-green-600">+{userData.credits.totalEarned}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Gesamt ausgegeben</span>
<span class="font-mono text-red-500">-{userData.credits.totalSpent}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Transaktionen</span>
<span class="font-mono">{userData.credits.transactionsCount}</span>
</div>
</div>
</div>
</div>
<!-- Project Data -->
<div>
<h3 class="text-lg font-semibold mb-4">Projektdaten</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each userData.projects as project}
<ProjectDataCard {project} />
{/each}
</div>
</div>
{/if}
</div>
<!-- Delete Confirmation Dialog -->
{#if showDeleteDialog}
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-card rounded-xl shadow-xl max-w-md w-full">
{#if deleteResult}
<!-- Success State -->
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<div
class="h-10 w-10 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center"
>
<svg
class="h-5 w-5 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 class="text-lg font-semibold">Loschung abgeschlossen</h3>
</div>
<p class="text-sm text-muted-foreground mb-4">
Insgesamt wurden <strong>{deleteResult.totalDeleted}</strong> Eintrage geloscht.
</p>
<div class="space-y-2 mb-6">
{#each deleteResult.deletedFromProjects as project}
<div class="flex items-center justify-between text-sm">
<span>{project.projectName}</span>
{#if project.success}
<span class="text-green-600">{project.deletedCount} geloscht</span>
{:else}
<span class="text-red-500">Fehler</span>
{/if}
</div>
{/each}
<div class="pt-2 border-t">
<div class="flex items-center justify-between text-sm">
<span>Sessions</span>
<span>{deleteResult.deletedFromAuth.sessions}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span>Accounts</span>
<span>{deleteResult.deletedFromAuth.accounts}</span>
</div>
</div>
</div>
<button
onclick={() => goto('/admin/user-data')}
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Zuruck zur Ubersicht
</button>
</div>
{:else}
<!-- Confirmation State -->
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<div
class="h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center"
>
<svg
class="h-5 w-5 text-red-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h3 class="text-lg font-semibold text-red-600">Daten unwiderruflich loschen?</h3>
</div>
<p class="text-sm text-muted-foreground mb-4">
Diese Aktion loscht <strong>alle Daten</strong> des Nutzers aus allen Projekten. Dies umfasst:
</p>
<ul class="text-sm text-muted-foreground mb-4 list-disc list-inside space-y-1">
<li>Alle Projektdaten (Chat, Todo, Calendar, etc.)</li>
<li>Alle Sessions und verknupften Accounts</li>
<li>Credits und Transaktionshistorie</li>
<li>Das Nutzerkonto selbst</li>
</ul>
<div class="mb-4">
<label for="delete-confirm-email" class="block text-sm font-medium mb-2">
Zur Bestätigung, geben Sie die Email-Adresse ein:
</label>
<input
id="delete-confirm-email"
type="email"
placeholder={userData?.user.email}
bind:value={deleteConfirmEmail}
class="w-full px-3 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
{#if deleteError}
<p class="text-sm text-red-500 mb-4">{deleteError}</p>
{/if}
<div class="flex gap-3">
<button
onclick={() => {
showDeleteDialog = false;
deleteConfirmEmail = '';
deleteError = null;
}}
class="flex-1 px-4 py-2 border rounded-lg hover:bg-muted transition-colors"
disabled={deleting}
>
Abbrechen
</button>
<button
onclick={handleDelete}
disabled={deleting || deleteConfirmEmail !== userData?.user.email}
class="flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{#if deleting}
Losche...
{:else}
Endgültig loschen
{/if}
</button>
</div>
</div>
{/if}
</div>
</div>
{/if}