'use client' import React, { useState, useCallback } from 'react' import { ChecklistView } from './ChecklistView' import { DocumentRow } from './DocumentRow' import { MigrationPanel } from './MigrationPanel' import { PreScanWizard, useScanContext, isContextComplete } from './PreScanWizard' import { DOCUMENT_TYPES, type DocTypeId } from './_document_types' import { STORAGE_KEY_STATE, STORAGE_KEY_RESULTS, STORAGE_KEY_HISTORY, STORAGE_KEY_CHECK_ID, countWords, initState, type DocState, type DocsState, type HistoryEntry, } from './_compliance_storage' import { useCompanyOrigin } from './_useCompanyOrigin' export function ComplianceCheckTab() { const [docs, setDocs] = useState(initState) const { companyName, setCompanyName, originDomain, setOriginDomain } = useCompanyOrigin() const [scanContext, setScanContext] = useScanContext() const [useAgent, setUseAgent] = useState(false) const [tdmOverride, setTdmOverride] = useState(false) const [tdmOverrideReason, setTdmOverrideReason] = useState('') const [loading, setLoading] = useState(false) const [progress, setProgress] = useState('') const [progressPct, setProgressPct] = useState(0) const [results, setResults] = useState(() => { if (typeof window === 'undefined') return null try { const s = localStorage.getItem(STORAGE_KEY_RESULTS); return s ? JSON.parse(s) : null } catch { return null } }) const [error, setError] = useState(null) const [activeCheckId, setActiveCheckId] = useState(() => typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY_CHECK_ID) || '' : '' ) const [history, setHistory] = useState(() => { if (typeof window === 'undefined') return [] try { return JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY) || '[]') } catch { return [] } }) // Persist URLs and texts (not loading/error state) React.useEffect(() => { const toSave: Record = {} for (const [key, val] of Object.entries(docs)) { toSave[key] = { url: val.url, text: val.text } } try { localStorage.setItem(STORAGE_KEY_STATE, JSON.stringify(toSave)) } catch { /* quota */ } }, [docs]) // Resume polling if check was in progress when navigating away React.useEffect(() => { if (!activeCheckId || results) return let cancelled = false setLoading(true) setProgress('Pruefung laeuft noch...') const poll = async () => { while (!cancelled) { await new Promise(r => setTimeout(r, 3000)) try { const res = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${activeCheckId}`) if (!res.ok) continue const data = await res.json() if (data.progress) setProgress(data.progress) if (typeof data.progress_pct === 'number') setProgressPct(data.progress_pct) if (data.status === 'completed' && data.result) { setResults(data.result); setProgress(''); setProgressPct(0); setLoading(false) localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(data.result)) localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') return } if (['failed', 'not_found', 'skipped_tdm'].includes(data.status)) { if (data.status !== 'not_found') setError(data.error || (data.status === 'skipped_tdm' ? 'TDM-Vorbehalt erkannt — Crawl uebersprungen' : 'Pruefung fehlgeschlagen')) setProgress(''); setProgressPct(0); setLoading(false); localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId(''); return } } catch { /* retry */ } } } poll() return () => { cancelled = true } }, []) // eslint-disable-line react-hooks/exhaustive-deps const updateDoc = useCallback((docType: DocTypeId, patch: Partial) => { setDocs(prev => ({ ...prev, [docType]: { ...prev[docType], ...patch } })) }, []) const handleFetchText = useCallback(async (docType: DocTypeId) => { const url = docs[docType].url.trim() if (!url) return updateDoc(docType, { loading: true, error: null }) try { const res = await fetch('/api/sdk/v1/agent/extract-text', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }) if (!res.ok) { const msg = res.status === 404 ? 'Seite nicht erreichbar' : `Fehler beim Laden (${res.status})` throw new Error(msg) } const data = await res.json() updateDoc(docType, { text: data.text || '', loading: false }) } catch (e) { updateDoc(docType, { loading: false, error: e instanceof Error ? e.message : 'Text konnte nicht geladen werden', }) } }, [docs, updateDoc]) const handleFileUpload = useCallback(async (docType: DocTypeId, file: File) => { // For now, read as text. PDF/DOCX parsing can be added server-side later. const reader = new FileReader() reader.onload = () => { updateDoc(docType, { text: reader.result as string }) } reader.readAsText(file) }, [updateDoc]) const filledCount = Object.values(docs).filter(d => d.url.trim() || d.text.trim()).length const handleSubmit = async () => { if (filledCount === 0) return setLoading(true) setError(null) setResults(null) setProgress('Compliance-Check wird gestartet...') setProgressPct(0) try { const entries = DOCUMENT_TYPES .filter(dt => docs[dt.id].url.trim() || docs[dt.id].text.trim()) .map(dt => ({ doc_type: dt.id, label: dt.label, url: docs[dt.id].url.trim(), text: docs[dt.id].text.trim() || undefined, })) const startRes = await fetch('/api/sdk/v1/agent/compliance-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documents: entries, use_agent: useAgent, tdm_override: tdmOverride && tdmOverrideReason.trim().length >= 10, tdm_override_reason: tdmOverrideReason.trim(), company_name: companyName.trim() || undefined, origin_domain: originDomain.trim() || undefined, // P79 — Pre-Scan-Wizard 8 Pflichtfelder; treibt MC-Scope-Filter (P72) scan_context: scanContext, }), }) if (!startRes.ok) throw new Error(`Pruefung konnte nicht gestartet werden: ${startRes.status}`) const { check_id } = await startRes.json() if (!check_id) throw new Error('Keine Check-ID erhalten') setActiveCheckId(check_id) localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id) // Poll for results (max 25 min = 500 polls x 3s) let attempts = 0 while (attempts < 500) { await new Promise(r => setTimeout(r, 3000)) const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`) if (!pollRes.ok) { attempts++; continue } const pollData = await pollRes.json() if (pollData.progress) setProgress(pollData.progress) if (typeof pollData.progress_pct === 'number') setProgressPct(pollData.progress_pct) if (pollData.status === 'completed' && pollData.result) { setResults(pollData.result) setProgress('') setProgressPct(0) localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(pollData.result)) localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') const resultKey = `compliance-check-result-${Date.now()}` try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch { /* quota */ } const entry: HistoryEntry = { date: new Date().toISOString(), docCount: entries.length, findings: pollData.result.total_findings || 0, resultKey, } const updated = [entry, ...history].slice(0, 30) setHistory(updated) localStorage.setItem(STORAGE_KEY_HISTORY, JSON.stringify(updated)) break } if (['failed', 'skipped_tdm'].includes(pollData.status)) { localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') throw new Error(pollData.error || (pollData.status === 'skipped_tdm' ? 'TDM-Vorbehalt' : 'Pruefung fehlgeschlagen')) } attempts++ } if (attempts >= 500) { localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') throw new Error('Zeitlimit ueberschritten (15 Min)') } } catch (e) { setError(e instanceof Error ? e.message : 'Unbekannter Fehler') setProgress('') setProgressPct(0) } finally { setLoading(false) } } const loadFromHistory = (entry: HistoryEntry) => { if (entry.resultKey) { try { const saved = localStorage.getItem(entry.resultKey) if (saved) { setResults(JSON.parse(saved)); return } } catch { /* ignore */ } } try { const last = localStorage.getItem(STORAGE_KEY_RESULTS) if (last) setResults(JSON.parse(last)) } catch { /* ignore */ } } const contextReady = isContextComplete(scanContext) return (
{/* Info box */}

Compliance-Check (Alle Dokumente)

Geben Sie die URLs Ihrer Rechtstexte ein oder laden Sie die Dokumente hoch. Das System prueft alle Pflichtangaben nach DSGVO, TDDDG, TMG und UWG. Pflichtdokumente sind mit * markiert.

{/* Firma + Domain (priorisiert vor extracted_profile-LLM-Inferenz) */}
{/* P79 Pre-Scan-Wizard — 8 Pflichtfelder zum MC-Scope-Filter (P72) */} {/* Document rows */}
{DOCUMENT_TYPES.map(dt => ( updateDoc(dt.id, { url })} onFetchText={() => handleFetchText(dt.id)} onTextChange={text => updateDoc(dt.id, { text })} onFileUpload={file => handleFileUpload(dt.id, file)} /> ))}
{/* Agent toggle + submit */}
{filledCount} von {DOCUMENT_TYPES.length} Dokumenten ausgefuellt
{tdmOverride && setTdmOverrideReason(e.target.value)} placeholder="z.B. Auftragsbeziehung Safetykon GmbH, Email Hr. X vom 18.05.2026" className="w-full px-3 py-2 text-xs border border-amber-300 rounded bg-white" />} {tdmOverride && tdmOverrideReason.trim().length < 10 &&

Pflicht: Reason mit min. 10 Zeichen (Audit-Spur).

}
{/* Submit button — Wizard muss vollstaendig sein (P79) */} {/* Progress */} {progress && (
{progress} {progressPct}%
)} {/* Error */} {error && (
{error}
)} {/* Results */} {results && results.results && (
{/* Business Profile */} {results.business_profile && (
Erkanntes Geschaeftsmodell
Typ: {results.business_profile.business_type?.toUpperCase()} Branche: {results.business_profile.industry} {results.business_profile.has_online_shop && Online-Shop} {results.business_profile.is_regulated_profession && Regulierter Beruf ({results.business_profile.regulated_profession_type})}
)} {/* Extracted Profile — pre-fill suggestion */} {results.extracted_profile?.company_profile && Object.keys(results.extracted_profile.company_profile).length > 0 && (
Aus Dokumenten extrahiert
{results.extracted_profile.company_profile.companyName && ( Firma: {results.extracted_profile.company_profile.companyName} )} {results.extracted_profile.company_profile.legalForm && ( Rechtsform: {results.extracted_profile.company_profile.legalForm.toUpperCase()} )} {results.extracted_profile.company_profile.headquartersCity && ( Sitz: {results.extracted_profile.company_profile.headquartersZip} {results.extracted_profile.company_profile.headquartersCity} )} {results.extracted_profile.company_profile.dpoEmail && ( DSB: {results.extracted_profile.company_profile.dpoEmail} )} {results.extracted_profile.company_profile.ustIdNr && ( USt-IdNr: {results.extracted_profile.company_profile.ustIdNr} )}
{results.extracted_profile.compliance_scope_hints?.length > 0 && (
Scope-Hinweise: {results.extracted_profile.compliance_scope_hints.map((h: any, i: number) => ( {h.source} ))}
)}
)} {/* Banner Check Result */} {results.banner_result && (
0 ? 'bg-amber-50 border-amber-200' : results.banner_result.detected ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200' }`}>
0 ? 'bg-amber-500' : results.banner_result.detected ? 'bg-green-500' : 'bg-gray-400' }`} /> Cookie-Banner-Check (automatisch)
{results.banner_result.detected ? ( <> Banner erkannt{results.banner_result.provider ? ` (${results.banner_result.provider})` : ''}. {results.banner_result.violations > 0 ? ` ${results.banner_result.violations} Auffaelligkeit${results.banner_result.violations !== 1 ? 'en' : ''} gefunden.` : ' Keine Auffaelligkeiten.'} ) : ( 'Kein Cookie-Banner erkannt oder Banner-Check nicht moeglich.' )}
)} {/* Email + Migration + Full-audit */} {results.email_status && (
E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status}
)} {results.check_id && }
)} {/* History */} {history.length > 0 && (

Letzte Compliance-Checks

{history.map((h, i) => ( ))}
)}
) }