All checks were successful
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) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard). SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest. Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
261 lines
11 KiB
TypeScript
261 lines
11 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState, useEffect } from 'react'
|
||
import Link from 'next/link'
|
||
import { RiskScoreGauge } from '@/components/sdk/use-case-assessment/RiskScoreGauge'
|
||
|
||
interface Assessment {
|
||
id: string
|
||
title: string
|
||
feasibility: string
|
||
risk_level: string
|
||
risk_score: number
|
||
domain: string
|
||
created_at: string
|
||
}
|
||
|
||
const FEASIBILITY_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||
YES: { bg: 'bg-green-100', text: 'text-green-700', label: 'Machbar' },
|
||
CONDITIONAL: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Bedingt' },
|
||
NO: { bg: 'bg-red-100', text: 'text-red-700', label: 'Nein' },
|
||
}
|
||
|
||
export default function UseCasesPage() {
|
||
const [assessments, setAssessments] = useState<Assessment[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [filterFeasibility, setFilterFeasibility] = useState<string>('all')
|
||
const [filterRisk, setFilterRisk] = useState<string>('all')
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [page, setPage] = useState(0)
|
||
const [totalCount, setTotalCount] = useState(0)
|
||
const PAGE_SIZE = 20
|
||
|
||
useEffect(() => {
|
||
fetchAssessments()
|
||
}, [page, searchQuery])
|
||
|
||
async function fetchAssessments() {
|
||
try {
|
||
setLoading(true)
|
||
const params = new URLSearchParams({
|
||
limit: String(PAGE_SIZE),
|
||
offset: String(page * PAGE_SIZE),
|
||
})
|
||
if (searchQuery) params.set('search', searchQuery)
|
||
|
||
const response = await fetch(`/api/sdk/v1/ucca/assessments?${params}`)
|
||
if (!response.ok) {
|
||
throw new Error('Fehler beim Laden der Assessments')
|
||
}
|
||
const data = await response.json()
|
||
setAssessments(data.assessments || [])
|
||
setTotalCount(data.total || data.assessments?.length || 0)
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const filtered = assessments.filter(a => {
|
||
if (filterFeasibility !== 'all' && a.feasibility !== filterFeasibility) return false
|
||
if (filterRisk !== 'all' && a.risk_level !== filterRisk) return false
|
||
return true
|
||
})
|
||
|
||
const stats = {
|
||
total: assessments.length,
|
||
feasible: assessments.filter(a => a.feasibility === 'YES').length,
|
||
conditional: assessments.filter(a => a.feasibility === 'CONDITIONAL').length,
|
||
rejected: assessments.filter(a => a.feasibility === 'NO').length,
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">Use Case Assessment</h1>
|
||
<p className="mt-1 text-gray-500">
|
||
KI-Anwendungsfaelle erfassen und auf Compliance pruefen
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href="/sdk/use-cases/new"
|
||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||
</svg>
|
||
Neues Assessment
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||
<div className="text-sm text-gray-500">Gesamt</div>
|
||
<div className="text-3xl font-bold text-gray-900">{stats.total}</div>
|
||
</div>
|
||
<div className="bg-white rounded-xl border border-green-200 p-6">
|
||
<div className="text-sm text-green-600">Machbar</div>
|
||
<div className="text-3xl font-bold text-green-600">{stats.feasible}</div>
|
||
</div>
|
||
<div className="bg-white rounded-xl border border-yellow-200 p-6">
|
||
<div className="text-sm text-yellow-600">Bedingt</div>
|
||
<div className="text-3xl font-bold text-yellow-600">{stats.conditional}</div>
|
||
</div>
|
||
<div className="bg-white rounded-xl border border-red-200 p-6">
|
||
<div className="text-sm text-red-600">Abgelehnt</div>
|
||
<div className="text-3xl font-bold text-red-600">{stats.rejected}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Search */}
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
placeholder="Assessments durchsuchen..."
|
||
value={searchQuery}
|
||
onChange={e => { setSearchQuery(e.target.value); setPage(0) }}
|
||
className="w-full px-4 py-2 pl-10 bg-white border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||
/>
|
||
<svg className="absolute left-3 top-2.5 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<div className="flex items-center gap-4 flex-wrap">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-gray-500">Machbarkeit:</span>
|
||
{['all', 'YES', 'CONDITIONAL', 'NO'].map(f => (
|
||
<button
|
||
key={f}
|
||
onClick={() => setFilterFeasibility(f)}
|
||
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
||
filterFeasibility === f
|
||
? 'bg-purple-600 text-white'
|
||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||
}`}
|
||
>
|
||
{f === 'all' ? 'Alle' : FEASIBILITY_STYLES[f]?.label || f}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-gray-500">Risiko:</span>
|
||
{['all', 'MINIMAL', 'LOW', 'MEDIUM', 'HIGH', 'UNACCEPTABLE'].map(f => (
|
||
<button
|
||
key={f}
|
||
onClick={() => setFilterRisk(f)}
|
||
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
||
filterRisk === f
|
||
? 'bg-purple-600 text-white'
|
||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||
}`}
|
||
>
|
||
{f === 'all' ? 'Alle' : f}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Error */}
|
||
{error && (
|
||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
|
||
{error}
|
||
<button onClick={fetchAssessments} className="ml-3 underline">Erneut versuchen</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Loading */}
|
||
{loading && (
|
||
<div className="text-center py-12 text-gray-500">Lade Assessments...</div>
|
||
)}
|
||
|
||
{/* Assessment List */}
|
||
{!loading && filtered.length > 0 && (
|
||
<div className="space-y-4">
|
||
{filtered.map(assessment => {
|
||
const feasibility = FEASIBILITY_STYLES[assessment.feasibility] || FEASIBILITY_STYLES.YES
|
||
return (
|
||
<Link
|
||
key={assessment.id}
|
||
href={`/sdk/use-cases/${assessment.id}`}
|
||
className="block bg-white rounded-xl border border-gray-200 p-6 hover:border-purple-300 hover:shadow-md transition-all"
|
||
>
|
||
<div className="flex items-center gap-6">
|
||
<RiskScoreGauge score={assessment.risk_score} riskLevel={assessment.risk_level} size="sm" />
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<h3 className="text-lg font-semibold text-gray-900">{assessment.title || 'Unbenanntes Assessment'}</h3>
|
||
<span className={`px-2 py-0.5 text-xs rounded-full ${feasibility.bg} ${feasibility.text}`}>
|
||
{feasibility.label}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||
<span>{assessment.domain}</span>
|
||
<span>{new Date(assessment.created_at).toLocaleDateString('de-DE')}</span>
|
||
</div>
|
||
</div>
|
||
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</div>
|
||
</Link>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{/* Pagination */}
|
||
{!loading && totalCount > PAGE_SIZE && (
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-sm text-gray-500">
|
||
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, totalCount)} von {totalCount}
|
||
</p>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => setPage(p => Math.max(0, p - 1))}
|
||
disabled={page === 0}
|
||
className="px-3 py-1 text-sm bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
Zurueck
|
||
</button>
|
||
<button
|
||
onClick={() => setPage(p => p + 1)}
|
||
disabled={(page + 1) * PAGE_SIZE >= totalCount}
|
||
className="px-3 py-1 text-sm bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
Weiter
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Empty State */}
|
||
{!loading && filtered.length === 0 && !error && (
|
||
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||
</svg>
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-gray-900">Noch keine Assessments</h3>
|
||
<p className="mt-2 text-gray-500 mb-4">
|
||
Erstellen Sie Ihr erstes Use Case Assessment, um die Compliance-Bewertung zu starten.
|
||
</p>
|
||
<Link
|
||
href="/sdk/use-cases/new"
|
||
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||
>
|
||
Erstes Assessment erstellen
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|