'use client' import { useState, useEffect, useCallback } from 'react' import { useParams } from 'next/navigation' type Clarification = { id: string question: string source: string category: 'manufacturer' | 'pattern_norm' | string norm_references?: string[] affected_hazard_ids: string[] affected_hazard_names: string[] status: 'open' | 'in_progress' | 'answered' | 'not_relevant' answer?: 'ja' | 'nein' | 'teilweise' | '' reasoning?: string answered_by?: string answered_at?: string assigned_to?: string } type ListResponse = { clarifications: Clarification[] open_count: number answered_count: number total: number } const CATEGORY_LABEL: Record = { manufacturer: 'Hersteller', pattern_norm: 'Norm / Pattern', } const STATUS_LABEL: Record = { open: 'Offen', in_progress: 'In Klärung', answered: 'Beantwortet', not_relevant: 'Nicht relevant', } const STATUS_COLOR: Record = { open: 'bg-orange-100 text-orange-800', in_progress: 'bg-yellow-100 text-yellow-800', answered: 'bg-green-100 text-green-800', not_relevant: 'bg-gray-100 text-gray-700', } export default function ClarificationsPage() { const params = useParams() const projectId = params.projectId as string const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [editing, setEditing] = useState(null) const [filter, setFilter] = useState<'all' | 'open' | 'answered'>('open') const [searchQuery, setSearchQuery] = useState('') const load = useCallback(async () => { setLoading(true) setError(null) try { const r = await fetch(`/api/sdk/v1/iace/projects/${projectId}/clarifications`) if (!r.ok) throw new Error(`HTTP ${r.status}`) const json: ListResponse = await r.json() setData(json) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setLoading(false) } }, [projectId]) useEffect(() => { load() }, [load]) const filtered = (data?.clarifications ?? []).filter(c => { if (filter === 'open' && (c.status === 'answered' || c.status === 'not_relevant')) return false if (filter === 'answered' && c.status !== 'answered' && c.status !== 'not_relevant') return false if (searchQuery) { const q = searchQuery.toLowerCase() if (!c.question.toLowerCase().includes(q) && !c.source.toLowerCase().includes(q)) return false } return true }) const groupedBySource: Record = {} for (const c of filtered) { const key = c.source if (!groupedBySource[key]) groupedBySource[key] = [] groupedBySource[key].push(c) } // CRA-Spur: zeige Banner, wenn mindestens eine Klaerung einen CRA-Bezug // hat (Norm-Referenz "2024/2847" oder "DIN EN 40000-1-2"). Die Banner // erinnert den Anwender daran, dass die CRA-Pflichten zwar bereits jetzt // dokumentiert werden, aber erst zum 11.12.2027 verpflichtend gelten. const hasCRAClarifications = (data?.clarifications ?? []).some(c => (c.norm_references ?? []).some(n => n.includes('2024/2847') || n.includes('40000-1-2')) ) return (

Klärungen mit dem Anlagenbauer

Standardisierte Prüffragen aus Norm- und Herstellerwissen. Eine Antwort gilt für alle referenzierten Gefährdungen.

{data && (
)} CSV PDF / Druck
{(['open', 'answered', 'all'] as const).map(f => ( ))}
setSearchQuery(e.target.value)} className="flex-1 max-w-sm border rounded px-3 py-1.5 text-sm" />
{!loading && hasCRAClarifications && (
Cyber Resilience Act (CRA) — Hinweis zur Geltung
Diese Klärungsliste enthält Fragen zur Verordnung (EU) 2024/2847 (CRA). Die CRA gilt für „Produkte mit digitalen Elementen", die ab dem 11.12.2027 auf dem EU-Markt bereitgestellt werden. Die hier dokumentierten Pflichten (SBOM, signierte Updates, CVD-Policy, Patch-SLA, Incident-Notification an ENISA) sollten bereits jetzt im Entwurf des Anlagenbauers berücksichtigt sein. Harmonisierter Standard: DIN EN 40000-1-2 (Entwurf 11/2025).
)} {loading &&
Lade Klärungen…
} {error &&
Fehler: {error}
} {!loading && data && Object.keys(groupedBySource).length === 0 && (
Keine Klärungen für die aktuelle Auswahl.
)} {!loading && data && Object.entries(groupedBySource).map(([source, items]) => (

{CATEGORY_LABEL[items[0].category] || items[0].category} {source}

{items.map(c => (
{c.question}
Betrifft {c.affected_hazard_ids.length} Gefährdung {c.affected_hazard_ids.length !== 1 ? 'en' : ''} {c.affected_hazard_names.length > 0 && ( — {c.affected_hazard_names.slice(0, 2).join('; ')}{c.affected_hazard_names.length > 2 ? `, +${c.affected_hazard_names.length - 2} weitere` : ''} )}
{c.norm_references && c.norm_references.length > 0 && (
Norm: {c.norm_references.join(' | ')}
)} {c.status === 'answered' && c.reasoning && (
Antwort ({c.answer}): {c.reasoning} {c.answered_by && ( — {c.answered_by}, {c.answered_at?.slice(0, 10)} )}
)}
{STATUS_LABEL[c.status]}
))}
))} {editing && ( setEditing(null)} onSaved={() => { setEditing(null) load() }} /> )}
) } function Badge({ color, label }: { color: string; label: string }) { return {label} } type Comment = { id: string; author: string; body: string; created_at: string } type HistoryEntry = { actor: string from_status?: string to_status?: string from_answer?: string to_answer?: string created_at: string } function AnswerModal({ clarification, projectId, onClose, onSaved, }: { clarification: Clarification & { assigned_to?: string } projectId: string onClose: () => void onSaved: () => void }) { const [status, setStatus] = useState(clarification.status) const [answer, setAnswer] = useState<'ja' | 'nein' | 'teilweise' | ''>( (clarification.answer as 'ja' | 'nein' | 'teilweise' | '') || '' ) const [reasoning, setReasoning] = useState(clarification.reasoning || '') const [answeredBy, setAnsweredBy] = useState(clarification.answered_by || '') const [assignedTo, setAssignedTo] = useState(clarification.assigned_to || '') const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [comments, setComments] = useState([]) const [history, setHistory] = useState([]) const [newComment, setNewComment] = useState('') const [postingComment, setPostingComment] = useState(false) useEffect(() => { fetch(`/api/sdk/v1/iace/projects/${projectId}/clarifications/${encodeURIComponent(clarification.id)}/detail`) .then(r => r.ok ? r.json() : null) .then(d => { if (!d) return setComments(d.comments || []) setHistory(d.history || []) }) .catch(() => {}) }, [projectId, clarification.id]) const save = async () => { setSaving(true) setError(null) try { const r = await fetch( `/api/sdk/v1/iace/projects/${projectId}/clarifications/${encodeURIComponent(clarification.id)}/answer`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, answer, reasoning, answered_by: answeredBy, assigned_to: assignedTo, question: clarification.question, source: clarification.source, category: clarification.category, norm_references: clarification.norm_references, }), } ) if (!r.ok) throw new Error(`HTTP ${r.status}`) onSaved() } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setSaving(false) } } const postComment = async () => { if (!newComment.trim()) return setPostingComment(true) try { const r = await fetch( `/api/sdk/v1/iace/projects/${projectId}/clarifications/${encodeURIComponent(clarification.id)}/comment`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author: answeredBy || assignedTo || 'unbekannt', body: newComment }), } ) if (r.ok) { const d = await r.json() if (d.comment) setComments(prev => [...prev, d.comment]) setNewComment('') } else { setError(`Kommentar HTTP ${r.status} — bitte zuerst Status setzen, damit der Klärungs-Datensatz angelegt wird.`) } } finally { setPostingComment(false) } } return (
e.stopPropagation()}>
{clarification.source}
{clarification.question}
setAssignedTo(e.target.value)} className="w-full border rounded p-2 text-sm" placeholder="z.B. anlagenbauer@fanuc.de" />
setAnsweredBy(e.target.value)} className="w-full border rounded p-2 text-sm" placeholder="Name oder Kürzel" />
{(['open', 'in_progress', 'answered', 'not_relevant'] as const).map(s => ( ))}
{(status === 'answered' || status === 'in_progress') && ( <>
{(['ja', 'teilweise', 'nein'] as const).map(a => ( ))}
)}