'use client' import React, { useEffect, useState, useCallback, use } from 'react' interface DocItem { id: string | null doc_type: string doc_type_label: string title: string content_md: string | null version: number requirements_coverage: Record status: string signed_by: string | null signed_at: string | null generated_at: string | null superseded_at: string | null } interface DocListResponse { project_id: string total: number items: DocItem[] } const STATUS_STYLE: Record = { draft: 'bg-yellow-100 text-yellow-800', reviewed: 'bg-blue-100 text-blue-800', approved: 'bg-green-100 text-green-800', superseded: 'bg-gray-200 text-gray-600', not_generated: 'bg-gray-100 text-gray-400', } const STATUS_LABEL: Record = { draft: 'Entwurf', reviewed: 'Geprueft', approved: 'Freigegeben', superseded: 'Veraltet', not_generated: 'Nicht erzeugt', } export default function DocumentsPage({ params, }: { params: Promise<{ projectId: string }> }) { const { projectId } = use(params) const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [generating, setGenerating] = useState(null) const [expanded, setExpanded] = useState(null) const [docContent, setDocContent] = useState>({}) // Generation params per doc type const [manufacturer, setManufacturer] = useState('') const [notifiedBody, setNotifiedBody] = useState('') const [securityContact, setSecurityContact] = useState('') // Approval form const [approving, setApproving] = useState(null) const [signedBy, setSignedBy] = useState('') const tenant = '00000000-0000-0000-0000-000000000001' const load = useCallback(async () => { try { const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/documents`, { headers: { 'X-Tenant-ID': tenant }, }) if (!res.ok) throw new Error(await res.text()) setData(await res.json()) } catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Laden') } finally { setLoading(false) } }, [projectId]) useEffect(() => { load() }, [load]) const generate = async (docType: string) => { setGenerating(docType) setError('') try { const body: Record = { doc_type: docType } if (docType === 'doc_eu_conformity') { if (manufacturer) body.manufacturer = manufacturer if (notifiedBody) body.notified_body = notifiedBody } if (docType === 'doc_cvd_policy' && securityContact) { body.security_contact = securityContact } const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/documents/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant }, body: JSON.stringify(body), }) if (!res.ok) throw new Error(await res.text()) const doc = await res.json() setDocContent(prev => ({ ...prev, [doc.id]: doc.content_md })) await load() } catch (e) { setError(e instanceof Error ? e.message : 'Generierung fehlgeschlagen') } finally { setGenerating(null) } } const loadContent = async (docId: string) => { if (docContent[docId]) { setExpanded(expanded === docId ? null : docId) return } try { const res = await fetch(`/api/sdk/v1/cra/documents/${docId}`, { headers: { 'X-Tenant-ID': tenant }, }) if (!res.ok) throw new Error(await res.text()) const doc = await res.json() setDocContent(prev => ({ ...prev, [docId]: doc.content_md })) setExpanded(docId) } catch (e) { setError(e instanceof Error ? e.message : 'Laden fehlgeschlagen') } } const approve = async (docId: string, status: string) => { if (!signedBy.trim()) { setError('Bitte Namen zur Freigabe eintragen.') return } setApproving(docId) setError('') try { const res = await fetch(`/api/sdk/v1/cra/documents/${docId}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant }, body: JSON.stringify({ signed_by: signedBy, status }), }) if (!res.ok) throw new Error(await res.text()) await load() } catch (e) { setError(e instanceof Error ? e.message : 'Freigabe fehlgeschlagen') } finally { setApproving(null) } } const download = (doc: DocItem) => { const content = docContent[doc.id || ''] || doc.content_md || '' if (!content) return const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${doc.doc_type}_v${doc.version}_${doc.id?.slice(0, 8)}.md` a.click() URL.revokeObjectURL(url) } if (loading) return

Laedt...

return (
← Zurueck zum Projekt

CRA-Dokumente

DoC (Annex VII), Technische Doku (Annex V), CVD-Policy, Update-Policy, SBOM-Bericht — generiert aus aktuellem Projektstand.

{error && (
{error}
)} {/* Generation params */}
Optionale Parameter fuer Generierung (Hersteller, NoBo, Security-Contact)
setManufacturer(e.target.value)} placeholder="Acme GmbH, Musterstr. 1, 80331 Muenchen" className="w-full px-3 py-2 border rounded text-sm" />
setNotifiedBody(e.target.value)} placeholder="TUEV Nord (NB-0044)" className="w-full px-3 py-2 border rounded text-sm" />
setSecurityContact(e.target.value)} placeholder="security@example.com" className="w-full px-3 py-2 border rounded text-sm" />
{data?.items.map(doc => (

{doc.doc_type_label}

{doc.version > 0 && ( v{doc.version} )} {STATUS_LABEL[doc.status]}
{doc.generated_at && (

Generiert: {new Date(doc.generated_at).toLocaleString('de-DE')} {doc.signed_by && doc.signed_at && ( <> · Freigegeben von {doc.signed_by} am {new Date(doc.signed_at).toLocaleString('de-DE')} )}

)} {doc.requirements_coverage && Object.keys(doc.requirements_coverage).length > 0 && (

Coverage: {String(doc.requirements_coverage.fields_filled || 0)} / {String(doc.requirements_coverage.fields_required || 0)} Pflichtfelder · {String(doc.requirements_coverage.annex_anchor || '')}

)}
{doc.id && ( )}
{expanded === doc.id && doc.id && docContent[doc.id] && (

Markdown-Vorschau

                    {docContent[doc.id]}
                  
{doc.status === 'draft' && (

Vor Freigabe pruefen ob alle [zu ergaenzen]-Stellen gefuellt sind.

setSignedBy(e.target.value)} placeholder="Name + Rolle des Freigebenden" className="flex-1 px-2 py-1 border rounded text-sm" />
)}
)}
))}
Hinweis: Diese Dokumente sind Skelette aus dem aktuellen Projektstand. Markdown-Format, manuelles Editieren + Unterzeichnung erforderlich vor Inverkehrbringen. PDF-Export folgt in Phase 5.5.
) }