Files
breakpilot-compliance/admin-compliance/app/sdk/cra/page.tsx
T
Benjamin Admin 6ca085ffc5 feat(cra): Datenblatt-Analyse-Frontend (Grenzen-Extraktion + Rückfragen)
DatasheetExtract auf /sdk/cra: Datenblatt einfügen (oder Beispiel OWIS/Zwick) →
POST /extract-datasheet → gefuellte ISO-12100-Grenzen mit Quellen-Zitat +
deterministisch erkannte Schnittstellen/Einheiten + gezielte Rückfragen fuer
leere Pflichtfelder (foreseeable_misuses, person_groups, …). Vorstufe fuer
'Projekt anlegen' → IACE-Grenzen-Prefill.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 19:20:14 +02:00

231 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { ClassificationBadge } from './_components/ClassificationBadge'
import { ReadinessCheck } from './_components/ReadinessCheck'
import { DatasheetExtract } from './_components/DatasheetExtract'
interface CRAProject {
id: string
name: string
description: string
cra_classification: string | null
conformity_path: string | null
status: string
created_at: string
}
const PATH_LABEL: Record<string, string> = {
self_assessment: 'Modul A (Selbstbewertung)',
harmonized_standard: 'Harmonisierte Norm (noch nicht verfügbar)',
eucc: 'EUCC-Zertifikat',
notified_body: 'Modul B+C (Benannte Stelle)',
}
const STATUS_LABEL: Record<string, string> = {
draft: 'Entwurf',
scoped: 'Intake erfasst',
classified: 'Klassifiziert',
path_selected: 'Pfad gewaehlt',
requirements_mapped: 'Requirements',
evidence_pending: 'Evidence',
gaps_open: 'Gaps offen',
remediation: 'Remediation',
ready_for_review: 'In Pruefung',
declaration_ready: 'DoC bereit',
post_market: 'Post-Market',
archived: 'Archiviert',
}
export default function CRAProjectsPage() {
const router = useRouter()
const [projects, setProjects] = useState<CRAProject[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [showModal, setShowModal] = useState(false)
const [newName, setNewName] = useState('')
const [newDescription, setNewDescription] = useState('')
const [creating, setCreating] = useState(false)
const tenantHeader = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const loadProjects = useCallback(async () => {
try {
const res = await fetch('/api/sdk/v1/cra/projects', {
headers: { 'X-Tenant-ID': tenantHeader },
})
if (!res.ok) throw new Error(await res.text())
const data = await res.json()
setProjects(data.projects || [])
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [])
useEffect(() => { loadProjects() }, [loadProjects])
const createProject = async () => {
if (!newName.trim()) return
setCreating(true)
setError('')
try {
const res = await fetch('/api/sdk/v1/cra/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantHeader },
body: JSON.stringify({ name: newName, description: newDescription }),
})
if (!res.ok) throw new Error(await res.text())
const project = await res.json()
router.push(`/sdk/cra/${project.id}/intake`)
} catch (e) {
setError(e instanceof Error ? e.message : 'Anlegen fehlgeschlagen')
} finally {
setCreating(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900">CRA Compliance</h1>
<p className="text-gray-600 mt-2">
Cyber Resilience Act Konformitaets-Workflow fuer Produkte mit digitalen Elementen.
</p>
<p className="text-sm text-gray-500 mt-1">
Fuer Entwickler / Tech-Experten. Hardware-CE-Risikobeurteilung siehe{' '}
<a href="/sdk/iace" className="text-blue-600 hover:underline">iACE</a>.
</p>
</div>
<ReadinessCheck onCreateProject={() => setShowModal(true)} />
<DatasheetExtract />
{/* Bridge: vom Readiness-Check in die kombinierte CE × Cyber-Analyse */}
<a
href="/sdk/iace/e79921be-c78a-47ab-8dfa-5fa48ba8a34a/cra"
className="mb-6 block rounded-xl border border-orange-300 bg-orange-50 p-5 hover:border-orange-400 hover:bg-orange-100 transition-colors"
>
<h3 className="text-base font-semibold text-gray-900">
Cyber trifft Safety kombinierte CE × Cyber-Analyse ansehen
</h3>
<p className="text-sm text-gray-600 mt-1">
Am Beispielprojekt Kistenhubgerät": wo ein Cyber-Risiko eine bereits gemilderte
CE-Gefährdung wieder öffnet — mit Management-Übersicht, Risiken und Maßnahmen. →
</p>
</a>
<div className="mb-4 px-4 py-2 bg-emerald-50 border border-emerald-200 rounded-lg text-xs text-emerald-800 flex items-start gap-2">
<span className="font-semibold">Quellen &amp; Lizenz:</span>
<span>
Inhalte gemaess <strong>EU-Verordnung 2024/2847 (Cyber Resilience Act)</strong> —
Lizenzregel R1 (EU_LAW, woertlich uebernehmbar). ENISA-Implementation-Guidance
ergaenzend (R1 EU_PUBLIC).{' '}
<a href="/sdk/licenses" className="underline">Quellenverzeichnis</a>
</span>
</div>
{error && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">
{error}
<button onClick={() => setError('')} className="ml-3 underline">Schliessen</button>
</div>
)}
<button
onClick={() => setShowModal(true)}
className="mb-6 w-full py-4 border-2 border-dashed border-red-300 rounded-xl text-red-600 hover:bg-red-50 hover:border-red-400 transition-colors font-medium"
>
+ Neues CRA-Projekt
</button>
{loading ? (
<div className="text-center text-gray-500 py-12">Laedt...</div>
) : projects.length === 0 ? (
<p className="text-center text-gray-500 mt-8">
Noch keine Projekte. Starten Sie Ihre erste CRA-Konformitaetsanalyse.
</p>
) : (
<div className="space-y-3">
<h2 className="text-lg font-semibold text-gray-800">Projekte</h2>
{projects.map(p => (
<a
key={p.id}
href={`/sdk/cra/${p.id}`}
className="block bg-white rounded-xl shadow-sm border border-gray-200 p-5 hover:shadow-md hover:border-red-300 transition-all"
>
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 truncate">{p.name}</h3>
{p.description && (
<p className="text-sm text-gray-500 mt-1 truncate">{p.description}</p>
)}
</div>
<div className="flex items-center gap-3 flex-shrink-0">
<ClassificationBadge value={p.cra_classification} size="sm" />
{p.conformity_path && (
<span className="px-2 py-0.5 text-xs rounded-full bg-purple-100 text-purple-800">
{PATH_LABEL[p.conformity_path] || p.conformity_path}
</span>
)}
<span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-700">
{STATUS_LABEL[p.status] || p.status}
</span>
<span className="text-xs text-gray-400">
{new Date(p.created_at).toLocaleDateString('de-DE')}
</span>
</div>
</div>
</a>
))}
</div>
)}
{showModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 p-6">
<h3 className="text-lg font-semibold mb-4">Neues CRA-Projekt anlegen</h3>
<div className="space-y-3">
<input
type="text"
placeholder="Projektname (z.B. SmartHome Gateway v3)"
value={newName}
onChange={e => setNewName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
/>
<textarea
placeholder="Kurzbeschreibung (optional)"
value={newDescription}
onChange={e => setNewDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
/>
</div>
<div className="flex gap-3 mt-5">
<button
onClick={() => { setShowModal(false); setNewName(''); setNewDescription('') }}
disabled={creating}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
>
Abbrechen
</button>
<button
onClick={createProject}
disabled={creating || !newName.trim()}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:bg-gray-300"
>
{creating ? 'Erstelle...' : 'Anlegen'}
</button>
</div>
</div>
</div>
)}
</div>
</div>
)
}