731076835d
(a) Labels: Module korrekt zugeordnet — Modul A = Selbstbewertung, Modul B+C =
benannte Stelle, EUCC = eigenes Zertifikat (nicht Modul H), "harmonisierte
Norm" ist kein Modul sondern Konformitätsvermutung. Für den CRA noch KEINE
harmonisierte Norm veröffentlicht → Kachel als "noch nicht verfügbar"
(erwartet ~2027), nicht wählbar, mit Hinweis. (page/path/documents-Labels.)
(b) Gating: wichtige Klasse II + kritische Produkte dürfen NICHT selbst bewerten;
harmonisierte Norm allein genügt dort nicht → ALLOWED_PATHS IMPORTANT_II/
CRITICAL = {eucc, notified_body}; DEFAULT_FOR II = notified_body. _PATH_HINT
entsprechend. Regressionstest test_cra_conformity_paths.py.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
228 lines
9.0 KiB
TypeScript
228 lines
9.0 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState, useEffect, useCallback } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { ClassificationBadge } from './_components/ClassificationBadge'
|
||
import { ReadinessCheck } from './_components/ReadinessCheck'
|
||
|
||
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)} />
|
||
|
||
{/* 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 & 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>
|
||
)
|
||
}
|