Extract components and hooks from oversized pages into colocated _components/ and _hooks/ subdirectories to enforce the 500-LOC hard cap. page.tsx files reduced to 205, 121, and 136 LOC respectively. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
import { Portfolio, api, statusLabels } from './_components/PortfolioTypes'
|
|
import { PortfolioCard } from './_components/PortfolioCard'
|
|
import { CreatePortfolioModal } from './_components/CreatePortfolioModal'
|
|
import { PortfolioDetailView } from './_components/PortfolioDetailView'
|
|
|
|
export default function PortfolioPage() {
|
|
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')
|
|
|
|
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>
|
|
)
|
|
}
|