Files
breakpilot-compliance/admin-compliance/app/sdk/portfolio/page.tsx
Benjamin Admin 37166c966f
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 18s
CI / test-python-dsms-gateway (push) Successful in 16s
feat(sdk): Audit-Dashboard + RBAC-Admin Frontends, UCCA/Go Cleanup
- Remove 5 unused UCCA routes (wizard, stats, dsb-pool) from Go main.go
- Delete 64 deprecated Go handlers (DSGVO, Vendors, Incidents, Drafting)
- Delete legacy proxy routes (dsgvo, vendors)
- Add LLM Audit Dashboard (3 tabs: Log, Nutzung, Compliance) with export
- Add RBAC Admin UI (5 tabs: Mandanten, Namespaces, Rollen, Benutzer, LLM-Policies)
- Add proxy routes for audit-llm and rbac to Go backend
- Add Workshop, Portfolio, Roadmap proxy routes and frontends
- Add LLM Audit + RBAC Admin to SDKSidebar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:45:56 +01:00

659 lines
26 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { useSDK } from '@/lib/sdk'
// =============================================================================
// TYPES
// =============================================================================
interface Portfolio {
id: string
name: string
description: string
status: 'DRAFT' | 'ACTIVE' | 'REVIEW' | 'APPROVED' | 'ARCHIVED'
department: string
business_unit: string
owner: string
owner_email: string
total_assessments: number
total_roadmaps: number
total_workshops: number
avg_risk_score: number
high_risk_count: number
compliance_score: number
auto_update_metrics: boolean
require_approval: boolean
created_at: string
updated_at: string
approved_at: string | null
approved_by: string | null
}
interface PortfolioItem {
id: string
portfolio_id: string
item_type: 'ASSESSMENT' | 'ROADMAP' | 'WORKSHOP' | 'DOCUMENT'
item_id: string
title: string
status: string
risk_level: string
risk_score: number
feasibility: string
sort_order: number
tags: string[]
notes: string
created_at: string
}
interface PortfolioStats {
total_items: number
items_by_type: Record<string, number>
risk_distribution: Record<string, number>
avg_risk_score: number
compliance_score: number
}
interface ActivityEntry {
timestamp: string
action: string
item_type: string
item_id: string
item_title: string
user_id: string
}
interface CompareResult {
portfolios: Portfolio[]
risk_scores: Record<string, number>
compliance_scores: Record<string, number>
item_counts: Record<string, number>
common_items: string[]
unique_items: Record<string, string[]>
}
// =============================================================================
// API
// =============================================================================
const API_BASE = '/api/sdk/v1/portfolio'
async function api<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
headers: { 'Content-Type': 'application/json' },
...options,
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || err.message || `HTTP ${res.status}`)
}
return res.json()
}
// =============================================================================
// COMPONENTS
// =============================================================================
const statusColors: Record<string, string> = {
DRAFT: 'bg-gray-100 text-gray-700',
ACTIVE: 'bg-green-100 text-green-700',
REVIEW: 'bg-yellow-100 text-yellow-700',
APPROVED: 'bg-purple-100 text-purple-700',
ARCHIVED: 'bg-red-100 text-red-700',
}
const statusLabels: Record<string, string> = {
DRAFT: 'Entwurf',
ACTIVE: 'Aktiv',
REVIEW: 'In Pruefung',
APPROVED: 'Genehmigt',
ARCHIVED: 'Archiviert',
}
function PortfolioCard({ portfolio, onSelect, onDelete }: {
portfolio: Portfolio
onSelect: (p: Portfolio) => void
onDelete: (id: string) => void
}) {
const totalItems = portfolio.total_assessments + portfolio.total_roadmaps + portfolio.total_workshops
return (
<div className="bg-white rounded-xl border-2 border-gray-200 p-6 hover:border-purple-300 transition-colors cursor-pointer"
onClick={() => onSelect(portfolio)}>
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-gray-900 truncate">{portfolio.name}</h4>
{portfolio.department && <span className="text-xs text-gray-500">{portfolio.department}</span>}
</div>
<span className={`px-2 py-1 text-xs rounded-full ml-2 ${statusColors[portfolio.status] || 'bg-gray-100 text-gray-700'}`}>
{statusLabels[portfolio.status] || portfolio.status}
</span>
</div>
{portfolio.description && (
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{portfolio.description}</p>
)}
<div className="grid grid-cols-3 gap-2 mb-4">
<div className="bg-gray-50 rounded-lg p-2 text-center">
<div className="text-lg font-bold text-purple-600">{portfolio.compliance_score}%</div>
<div className="text-xs text-gray-500">Compliance</div>
</div>
<div className="bg-gray-50 rounded-lg p-2 text-center">
<div className="text-lg font-bold text-gray-900">{portfolio.avg_risk_score.toFixed(1)}</div>
<div className="text-xs text-gray-500">Risiko</div>
</div>
<div className="bg-gray-50 rounded-lg p-2 text-center">
<div className="text-lg font-bold text-gray-900">{totalItems}</div>
<div className="text-xs text-gray-500">Items</div>
</div>
</div>
{portfolio.high_risk_count > 0 && (
<div className="flex items-center gap-1 text-xs text-red-600 mb-3">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
{portfolio.high_risk_count} Hoch-Risiko
</div>
)}
<div className="flex justify-between items-center">
<span className="text-xs text-gray-400">{portfolio.owner || 'Kein Owner'}</span>
<button onClick={(e) => { e.stopPropagation(); onDelete(portfolio.id) }}
className="text-xs text-red-500 hover:text-red-700 hover:bg-red-50 px-2 py-1 rounded">
Loeschen
</button>
</div>
</div>
)
}
function CreatePortfolioModal({ onClose, onCreated }: {
onClose: () => void
onCreated: () => void
}) {
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [department, setDepartment] = useState('')
const [owner, setOwner] = useState('')
const [saving, setSaving] = useState(false)
const handleCreate = async () => {
if (!name.trim()) return
setSaving(true)
try {
await api('', {
method: 'POST',
body: JSON.stringify({
name: name.trim(),
description: description.trim(),
department: department.trim(),
owner: owner.trim(),
}),
})
onCreated()
} catch (err) {
console.error('Create portfolio error:', err)
} finally {
setSaving(false)
}
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-white rounded-2xl p-6 w-full max-w-lg" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-bold text-gray-900 mb-4">Neues Portfolio</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name *</label>
<input type="text" value={name} onChange={e => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="z.B. KI-Portfolio Q1 2026" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
<textarea value={description} onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
rows={3} />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Abteilung</label>
<input type="text" value={department} onChange={e => setDepartment(e.target.value)}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verantwortlicher</label>
<input type="text" value={owner} onChange={e => setOwner(e.target.value)}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" />
</div>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
<button onClick={handleCreate} disabled={!name.trim() || saving}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">
{saving ? 'Erstelle...' : 'Erstellen'}
</button>
</div>
</div>
</div>
)
}
function PortfolioDetailView({ portfolio, onBack, onRefresh }: {
portfolio: Portfolio
onBack: () => void
onRefresh: () => void
}) {
const [items, setItems] = useState<PortfolioItem[]>([])
const [activity, setActivity] = useState<ActivityEntry[]>([])
const [stats, setStats] = useState<PortfolioStats | null>(null)
const [activeTab, setActiveTab] = useState<'items' | 'activity' | 'compare'>('items')
const [loading, setLoading] = useState(true)
const [compareIds, setCompareIds] = useState('')
const [compareResult, setCompareResult] = useState<CompareResult | null>(null)
const loadDetails = useCallback(async () => {
setLoading(true)
try {
const [i, a, s] = await Promise.all([
api<PortfolioItem[]>(`/${portfolio.id}/items`).catch(() => []),
api<ActivityEntry[]>(`/${portfolio.id}/activity`).catch(() => []),
api<PortfolioStats>(`/${portfolio.id}/stats`).catch(() => null),
])
setItems(Array.isArray(i) ? i : [])
setActivity(Array.isArray(a) ? a : [])
setStats(s)
} finally {
setLoading(false)
}
}, [portfolio.id])
useEffect(() => { loadDetails() }, [loadDetails])
const handleSubmitReview = async () => {
try {
await api(`/${portfolio.id}/submit-review`, { method: 'POST' })
onRefresh()
} catch (err) {
console.error('Submit review error:', err)
}
}
const handleApprove = async () => {
try {
await api(`/${portfolio.id}/approve`, { method: 'POST' })
onRefresh()
} catch (err) {
console.error('Approve error:', err)
}
}
const handleRecalculate = async () => {
try {
await api(`/${portfolio.id}/recalculate`, { method: 'POST' })
loadDetails()
onRefresh()
} catch (err) {
console.error('Recalculate error:', err)
}
}
const handleCompare = async () => {
const ids = compareIds.split(',').map(s => s.trim()).filter(Boolean)
if (ids.length < 1) return
try {
const result = await api<CompareResult>('/compare', {
method: 'POST',
body: JSON.stringify({ portfolio_ids: [portfolio.id, ...ids] }),
})
setCompareResult(result)
} catch (err) {
console.error('Compare error:', err)
}
}
const handleRemoveItem = async (itemId: string) => {
try {
await api(`/${portfolio.id}/items/${itemId}`, { method: 'DELETE' })
setItems(prev => prev.filter(i => i.id !== itemId))
} catch (err) {
console.error('Remove item error:', err)
}
}
const typeLabels: Record<string, string> = {
ASSESSMENT: 'Assessment', ROADMAP: 'Roadmap', WORKSHOP: 'Workshop', DOCUMENT: 'Dokument',
}
const typeColors: Record<string, string> = {
ASSESSMENT: 'bg-blue-100 text-blue-700', ROADMAP: 'bg-green-100 text-green-700',
WORKSHOP: 'bg-purple-100 text-purple-700', DOCUMENT: 'bg-orange-100 text-orange-700',
}
return (
<div>
<button onClick={onBack} className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 mb-4">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zur Uebersicht
</button>
<div className="bg-white rounded-xl border-2 border-gray-200 p-6 mb-6">
<div className="flex items-start justify-between mb-4">
<div>
<h2 className="text-xl font-bold text-gray-900">{portfolio.name}</h2>
<p className="text-sm text-gray-500 mt-1">{portfolio.description}</p>
</div>
<span className={`px-3 py-1 text-sm rounded-full ${statusColors[portfolio.status]}`}>
{statusLabels[portfolio.status]}
</span>
</div>
{stats && (
<div className="grid grid-cols-4 gap-4 mb-4">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-purple-600">{stats.compliance_score}%</div>
<div className="text-xs text-gray-500">Compliance</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-900">{stats.avg_risk_score.toFixed(1)}</div>
<div className="text-xs text-gray-500">Risiko-Score</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-gray-900">{stats.total_items}</div>
<div className="text-xs text-gray-500">Items</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-red-600">{portfolio.high_risk_count}</div>
<div className="text-xs text-gray-500">Hoch-Risiko</div>
</div>
</div>
)}
<div className="flex gap-2">
{portfolio.status === 'ACTIVE' && (
<button onClick={handleSubmitReview} className="px-3 py-1.5 text-sm bg-yellow-600 text-white rounded-lg hover:bg-yellow-700">
Zur Pruefung einreichen
</button>
)}
{portfolio.status === 'REVIEW' && (
<button onClick={handleApprove} className="px-3 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700">
Genehmigen
</button>
)}
<button onClick={handleRecalculate} className="px-3 py-1.5 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">
Metriken neu berechnen
</button>
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 mb-4 bg-gray-100 p-1 rounded-lg">
{(['items', 'activity', 'compare'] as const).map(tab => (
<button key={tab} onClick={() => setActiveTab(tab)}
className={`flex-1 px-4 py-2 text-sm rounded-md transition-colors ${activeTab === tab ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}`}>
{tab === 'items' ? `Items (${items.length})` : tab === 'activity' ? 'Aktivitaet' : 'Vergleich'}
</button>
))}
</div>
{loading ? (
<div className="text-center py-8 text-gray-500">Laden...</div>
) : (
<>
{activeTab === 'items' && (
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Titel</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Typ</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Risiko</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Aktion</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{items.map(item => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<div className="font-medium text-gray-900">{item.title}</div>
{item.notes && <div className="text-xs text-gray-500 truncate max-w-xs">{item.notes}</div>}
</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 text-xs rounded-full ${typeColors[item.item_type] || 'bg-gray-100 text-gray-700'}`}>
{typeLabels[item.item_type] || item.item_type}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-600">{item.status}</td>
<td className="px-4 py-3">
<span className={`text-sm font-medium ${
item.risk_score >= 7 ? 'text-red-600' : item.risk_score >= 4 ? 'text-yellow-600' : 'text-green-600'
}`}>{item.risk_score.toFixed(1)}</span>
</td>
<td className="px-4 py-3">
<button onClick={() => handleRemoveItem(item.id)}
className="text-xs text-red-500 hover:text-red-700">Entfernen</button>
</td>
</tr>
))}
{items.length === 0 && (
<tr><td colSpan={5} className="px-4 py-8 text-center text-gray-500">Keine Items</td></tr>
)}
</tbody>
</table>
</div>
)}
{activeTab === 'activity' && (
<div className="space-y-3">
{activity.map((a, i) => (
<div key={i} className="bg-white rounded-lg border border-gray-200 p-4 flex items-center gap-4">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white text-xs ${
a.action === 'added' ? 'bg-green-500' : a.action === 'removed' ? 'bg-red-500' : 'bg-blue-500'
}`}>
{a.action === 'added' ? '+' : a.action === 'removed' ? '-' : '~'}
</div>
<div className="flex-1">
<div className="text-sm text-gray-900">
<span className="font-medium">{a.item_title || a.item_id}</span> {a.action}
</div>
<div className="text-xs text-gray-500">{a.item_type}</div>
</div>
<div className="text-xs text-gray-400">{new Date(a.timestamp).toLocaleString('de-DE')}</div>
</div>
))}
{activity.length === 0 && (
<div className="text-center py-8 text-gray-500">Keine Aktivitaet</div>
)}
</div>
)}
{activeTab === 'compare' && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Portfolio-Vergleich</h3>
<div className="flex gap-3 mb-4">
<input
type="text" value={compareIds} onChange={e => setCompareIds(e.target.value)}
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
placeholder="Portfolio-IDs (kommagetrennt)"
/>
<button onClick={handleCompare}
className="px-4 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700">
Vergleichen
</button>
</div>
{compareResult && (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500">Portfolio</th>
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500">Risiko-Score</th>
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500">Compliance</th>
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500">Items</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{compareResult.portfolios?.map(p => (
<tr key={p.id}>
<td className="px-4 py-3 font-medium text-gray-900">{p.name}</td>
<td className="px-4 py-3 text-center">{compareResult.risk_scores?.[p.id]?.toFixed(1) ?? '-'}</td>
<td className="px-4 py-3 text-center">{compareResult.compliance_scores?.[p.id] ?? '-'}%</td>
<td className="px-4 py-3 text-center">{compareResult.item_counts?.[p.id] ?? '-'}</td>
</tr>
))}
</tbody>
</table>
{compareResult.common_items?.length > 0 && (
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
<div className="text-xs font-medium text-gray-500 mb-1">Gemeinsame Items: {compareResult.common_items.length}</div>
</div>
)}
</div>
)}
</div>
)}
</>
)}
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function PortfolioPage() {
const { setCurrentModule } = useSDK()
const [portfolios, setPortfolios] = useState<Portfolio[]>([])
const [loading, setLoading] = useState(true)
const [showCreate, setShowCreate] = useState(false)
const [selectedPortfolio, setSelectedPortfolio] = useState<Portfolio | null>(null)
const [filter, setFilter] = useState<string>('all')
useEffect(() => {
setCurrentModule('portfolio')
}, [setCurrentModule])
const loadPortfolios = useCallback(async () => {
setLoading(true)
try {
const data = await api<Portfolio[] | { portfolios: Portfolio[] }>('')
const list = Array.isArray(data) ? data : (data.portfolios || [])
setPortfolios(list)
} catch (err) {
console.error('Load portfolios error:', err)
} finally {
setLoading(false)
}
}, [])
useEffect(() => { loadPortfolios() }, [loadPortfolios])
const handleDelete = async (id: string) => {
if (!confirm('Portfolio wirklich loeschen?')) return
try {
await api(`/${id}`, { method: 'DELETE' })
setPortfolios(prev => prev.filter(p => p.id !== id))
} catch (err) {
console.error('Delete error:', err)
}
}
const filteredPortfolios = filter === 'all'
? portfolios
: portfolios.filter(p => p.status === filter)
if (selectedPortfolio) {
return (
<div className="p-6 max-w-6xl mx-auto">
<PortfolioDetailView
portfolio={selectedPortfolio}
onBack={() => { setSelectedPortfolio(null); loadPortfolios() }}
onRefresh={() => {
loadPortfolios().then(() => {
const updated = portfolios.find(p => p.id === selectedPortfolio.id)
if (updated) setSelectedPortfolio(updated)
})
}}
/>
</div>
)
}
return (
<div className="p-6 max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">KI-Portfolios</h1>
<p className="text-sm text-gray-500 mt-1">
Verwaltung und Vergleich von Compliance-Portfolios
</p>
</div>
<button onClick={() => setShowCreate(true)}
className="px-4 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neues Portfolio
</button>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-4 mb-6">
{[
{ label: 'Gesamt', value: portfolios.length, color: 'text-gray-900' },
{ label: 'Aktiv', value: portfolios.filter(p => p.status === 'ACTIVE').length, color: 'text-green-600' },
{ label: 'In Pruefung', value: portfolios.filter(p => p.status === 'REVIEW').length, color: 'text-yellow-600' },
{ label: 'Genehmigt', value: portfolios.filter(p => p.status === 'APPROVED').length, color: 'text-purple-600' },
].map(stat => (
<div key={stat.label} className="bg-white rounded-xl border border-gray-200 p-4 text-center">
<div className={`text-2xl font-bold ${stat.color}`}>{stat.value}</div>
<div className="text-xs text-gray-500">{stat.label}</div>
</div>
))}
</div>
{/* Filter */}
<div className="flex gap-2 mb-6">
{['all', 'DRAFT', 'ACTIVE', 'REVIEW', 'APPROVED'].map(f => (
<button key={f} onClick={() => setFilter(f)}
className={`px-3 py-1.5 text-sm rounded-lg ${filter === f ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}>
{f === 'all' ? 'Alle' : statusLabels[f] || f}
</button>
))}
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Portfolios werden geladen...</div>
) : filteredPortfolios.length === 0 ? (
<div className="text-center py-12">
<div className="text-gray-400 mb-2">
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<p className="text-gray-500">Keine Portfolios gefunden</p>
<button onClick={() => setShowCreate(true)} className="mt-3 text-sm text-purple-600 hover:text-purple-700">
Erstes Portfolio erstellen
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredPortfolios.map(p => (
<PortfolioCard key={p.id} portfolio={p} onSelect={setSelectedPortfolio} onDelete={handleDelete} />
))}
</div>
)}
{showCreate && (
<CreatePortfolioModal onClose={() => setShowCreate(false)} onCreated={() => { setShowCreate(false); loadPortfolios() }} />
)}
</div>
)
}