feat(frontend): persistent gap projects — list, create, re-analyze

- Project list view with saved projects
- Create + analyze in one flow (saves to DB)
- Re-open saved projects for re-analysis
- 3 views: projects list → wizard → dashboard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-11 07:50:03 +02:00
parent 6bd09d7676
commit 6cb5da56b3
+138 -14
View File
@@ -1,9 +1,17 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState, useEffect, useCallback } from 'react'
import { ProductWizard } from './_components/ProductWizard' import { ProductWizard } from './_components/ProductWizard'
import { GapDashboard } from './_components/GapDashboard' import { GapDashboard } from './_components/GapDashboard'
interface GapProject {
id: string
name: string
description: string
product_type: string
created_at: string
}
interface GapReport { interface GapReport {
profile_id: string profile_id: string
profile_name: string profile_name: string
@@ -39,23 +47,80 @@ interface GapReport {
}> }>
} }
type View = 'projects' | 'wizard' | 'dashboard'
const PRODUCT_TYPE_LABELS: Record<string, string> = {
iot: 'IoT', software: 'Software', saas: 'SaaS', hardware: 'Hardware',
machinery: 'Maschine', medical_device: 'Medizin', exchange: 'Fintech', other: 'Sonstiges',
}
export default function GapAnalysisPage() { export default function GapAnalysisPage() {
const [view, setView] = useState<View>('projects')
const [projects, setProjects] = useState<GapProject[]>([])
const [report, setReport] = useState<GapReport | null>(null) const [report, setReport] = useState<GapReport | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const handleAnalyze = async (profile: Record<string, unknown>) => { const loadProjects = useCallback(async () => {
try {
const res = await fetch('/api/sdk/v1/gap/projects', {
headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000001' },
})
if (res.ok) {
const data = await res.json()
setProjects(data.projects || [])
}
} catch { /* ignore */ }
}, [])
useEffect(() => { loadProjects() }, [loadProjects])
const handleCreateAndAnalyze = async (profile: Record<string, unknown>) => {
setLoading(true) setLoading(true)
setError('') setError('')
try { try {
const res = await fetch('/api/sdk/v1/gap/analyze', { // Save project
const createRes = await fetch('/api/sdk/v1/gap/projects', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': '00000000-0000-0000-0000-000000000001',
},
body: JSON.stringify(profile), body: JSON.stringify(profile),
}) })
if (!createRes.ok) throw new Error('Projekt konnte nicht gespeichert werden')
const created = await createRes.json()
const projectId = created.project?.id
// Run analysis
const analyzeRes = await fetch(`/api/sdk/v1/gap/projects/${projectId}/analyze`, {
method: 'POST',
headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000001' },
})
if (!analyzeRes.ok) throw new Error(await analyzeRes.text())
const data = await analyzeRes.json()
setReport(data)
setView('dashboard')
loadProjects()
} catch (e) {
setError(e instanceof Error ? e.message : 'Analyse fehlgeschlagen')
} finally {
setLoading(false)
}
}
const handleOpenProject = async (projectId: string) => {
setLoading(true)
setError('')
try {
const res = await fetch(`/api/sdk/v1/gap/projects/${projectId}/analyze`, {
method: 'POST',
headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000001' },
})
if (!res.ok) throw new Error(await res.text()) if (!res.ok) throw new Error(await res.text())
const data = await res.json() const data = await res.json()
setReport(data) setReport(data)
setView('dashboard')
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : 'Analyse fehlgeschlagen') setError(e instanceof Error ? e.message : 'Analyse fehlgeschlagen')
} finally { } finally {
@@ -66,29 +131,88 @@ export default function GapAnalysisPage() {
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4"> <div className="max-w-6xl mx-auto px-4">
<div className="mb-8"> <div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900"> <h1 className="text-3xl font-bold text-gray-900">
Regulatory Gap-Analyse Regulatory Gap-Analyse
</h1> </h1>
<p className="text-gray-600 mt-2"> <p className="text-gray-600 mt-2">
Beschreiben Sie Ihr Produkt und erhalten Sie eine priorisierte Produkt beschreiben, Regulierungen erkennen, Prioritaeten setzen.
Liste der Compliance-Anforderungen.
</p> </p>
</div> </div>
{view !== 'projects' && (
<button
onClick={() => { setView('projects'); setReport(null) }}
className="px-4 py-2 text-sm text-blue-600 hover:text-blue-800 border border-blue-200 rounded-lg hover:bg-blue-50"
>
Alle Projekte
</button>
)}
</div>
{error && ( {error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4"> <div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-700">{error}</p> <p className="text-red-700">{error}</p>
<button onClick={() => setError('')} className="text-sm text-red-500 mt-1 underline">
Schliessen
</button>
</div> </div>
)} )}
{!report ? ( {view === 'projects' && (
<ProductWizard onAnalyze={handleAnalyze} loading={loading} /> <div>
) : ( {/* New project button */}
<GapDashboard <button
report={report} onClick={() => setView('wizard')}
onBack={() => setReport(null)} className="mb-6 w-full py-4 border-2 border-dashed border-blue-300 rounded-xl text-blue-600 hover:bg-blue-50 hover:border-blue-400 transition-colors font-medium"
/> >
+ Neues Produkt analysieren
</button>
{/* Project list */}
{projects.length > 0 && (
<div className="space-y-3">
<h2 className="text-lg font-semibold text-gray-800">Gespeicherte Projekte</h2>
{projects.map(p => (
<button
key={p.id}
onClick={() => handleOpenProject(p.id)}
disabled={loading}
className="w-full text-left bg-white rounded-xl shadow-sm border border-gray-200 p-5 hover:shadow-md hover:border-blue-300 transition-all disabled:opacity-50"
>
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-gray-900">{p.name}</h3>
<p className="text-sm text-gray-500 mt-1">{p.description}</p>
</div>
<div className="flex items-center gap-3">
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium">
{PRODUCT_TYPE_LABELS[p.product_type] || p.product_type}
</span>
<span className="text-xs text-gray-400">
{new Date(p.created_at).toLocaleDateString('de-DE')}
</span>
</div>
</div>
</button>
))}
</div>
)}
{projects.length === 0 && (
<p className="text-center text-gray-500 mt-8">
Noch keine Projekte. Starten Sie Ihre erste Gap-Analyse.
</p>
)}
</div>
)}
{view === 'wizard' && (
<ProductWizard onAnalyze={handleCreateAndAnalyze} loading={loading} />
)}
{view === 'dashboard' && report && (
<GapDashboard report={report} onBack={() => { setView('projects'); setReport(null) }} />
)} )}
</div> </div>
</div> </div>