Files
breakpilot-compliance/admin-compliance/app/sdk/compliance-scope/page.tsx
Benjamin Admin 5da93c5d10
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
feat(regulations): Automatische Ableitung anwendbarer Gesetze & Aufsichtsbehoerden
Nach Abschluss von Profil + Scope werden jetzt automatisch die anwendbaren
Regulierungen (DSGVO, NIS2, AI Act, DORA) ermittelt und die zustaendigen
Aufsichtsbehoerden (Landes-DSB, BSI, BaFin) aus Bundesland + Branche abgeleitet.

- Neues scope-to-facts.ts: Mapping CompanyProfile+Scope → Go SDK Payload
- Neues supervisory-authority-resolver.ts: 16 Landes-DSB + nationale Behoerden
- ScopeDecisionTab: Regulierungs-Report mit Aufsichtsbehoerden-Karten
- Obligations-Seite: Echte Daten statt Dummy in handleAutoProfiling()
- Neue Types: ApplicableRegulation, RegulationAssessmentResult, SupervisoryAuthorityInfo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:29:24 +01:00

454 lines
17 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader/StepHeader'
import {
ScopeOverviewTab,
ScopeWizardTab,
ScopeDecisionTab,
ScopeExportTab
} from '@/components/sdk/compliance-scope'
import type {
ComplianceScopeState,
ScopeProfilingAnswer,
ScopeDecision,
ApplicableRegulation,
SupervisoryAuthorityInfo
} from '@/lib/sdk/compliance-scope-types'
import {
createEmptyScopeState,
STORAGE_KEY
} from '@/lib/sdk/compliance-scope-types'
import { complianceScopeEngine } from '@/lib/sdk/compliance-scope-engine'
import { getAllQuestions } from '@/lib/sdk/compliance-scope-profiling'
import { buildAssessmentPayload } from '@/lib/sdk/scope-to-facts'
import { resolveAuthorities } from '@/lib/sdk/supervisory-authority-resolver'
type TabId = 'overview' | 'wizard' | 'decision' | 'export'
const TABS: { id: TabId; label: string; icon: string }[] = [
{ id: 'overview', label: 'Uebersicht', icon: '📊' },
{ id: 'wizard', label: 'Scope-Profiling', icon: '📋' },
{ id: 'decision', label: 'Scope-Entscheidung', icon: '⚖️' },
{ id: 'export', label: 'Export', icon: '📤' },
]
export default function ComplianceScopePage() {
const { state: sdkState, dispatch } = useSDK()
// Active tab state
const [activeTab, setActiveTab] = useState<TabId>('overview')
// Local scope state
const [scopeState, setScopeState] = useState<ComplianceScopeState>(() => {
// Try to load from SDK context first
if (sdkState.complianceScope) {
return sdkState.complianceScope
}
return createEmptyScopeState()
})
// Loading state
const [isLoading, setIsLoading] = useState(true)
const [isEvaluating, setIsEvaluating] = useState(false)
// Regulation assessment state
const [applicableRegulations, setApplicableRegulations] = useState<ApplicableRegulation[]>([])
const [supervisoryAuthorities, setSupervisoryAuthorities] = useState<SupervisoryAuthorityInfo[]>([])
const [regulationAssessmentLoading, setRegulationAssessmentLoading] = useState(false)
// Load from SDK context first (persisted via State API), then localStorage as fallback.
// Runs ONCE on mount only — empty deps breaks the dispatch→sdkState→setScopeState→dispatch loop.
useEffect(() => {
try {
// Priority 1: SDK context (loaded from PostgreSQL via State API)
const ctxScope = sdkState.complianceScope
if (ctxScope && ctxScope.answers?.length > 0) {
setScopeState(ctxScope)
localStorage.setItem(STORAGE_KEY, JSON.stringify(ctxScope))
} else {
// Priority 2: localStorage fallback
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored) as ComplianceScopeState
setScopeState(parsed)
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed })
}
}
} catch (error) {
console.error('Failed to load compliance scope state:', error)
} finally {
setIsLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Fetch regulation assessment if decision exists on mount
useEffect(() => {
if (!isLoading && scopeState.decision && applicableRegulations.length === 0 && sdkState.companyProfile) {
fetchRegulationAssessment(scopeState.decision)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading])
// Save to localStorage and SDK context whenever state changes
useEffect(() => {
if (!isLoading) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(scopeState))
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scopeState })
} catch (error) {
console.error('Failed to save compliance scope state:', error)
}
}
}, [scopeState, isLoading, dispatch])
// Handle answers change from wizard
const handleAnswersChange = useCallback((answers: ScopeProfilingAnswer[]) => {
setScopeState(prev => ({
...prev,
answers,
lastModified: new Date().toISOString(),
}))
}, [])
// Fetch regulation assessment from Go AI SDK
const fetchRegulationAssessment = useCallback(async (decision: ScopeDecision) => {
const profile = sdkState.companyProfile
if (!profile) return
setRegulationAssessmentLoading(true)
try {
const payload = buildAssessmentPayload(profile, scopeState.answers, decision)
const res = await fetch('/api/sdk/v1/ucca/obligations/assess-from-scope', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
// Set applicable regulations from response
const regs: ApplicableRegulation[] = data.overview?.applicable_regulations || data.applicable_regulations || []
setApplicableRegulations(regs)
// Derive supervisory authorities
const regIds = regs.map(r => r.id)
const authorities = resolveAuthorities(
profile.headquartersState,
profile.headquartersCountry || 'DE',
regIds
)
setSupervisoryAuthorities(authorities)
} catch (error) {
console.error('Failed to fetch regulation assessment:', error)
} finally {
setRegulationAssessmentLoading(false)
}
}, [sdkState.companyProfile, scopeState.answers])
// Handle evaluate button click
const handleEvaluate = useCallback(async () => {
setIsEvaluating(true)
try {
// Run the compliance scope engine
const decision = complianceScopeEngine.evaluate(scopeState.answers)
// Update state with decision
setScopeState(prev => ({
...prev,
decision,
lastModified: new Date().toISOString(),
}))
// Switch to decision tab to show results
setActiveTab('decision')
// Fetch regulation assessment in the background
fetchRegulationAssessment(decision)
} catch (error) {
console.error('Failed to evaluate compliance scope:', error)
} finally {
setIsEvaluating(false)
}
}, [scopeState.answers, fetchRegulationAssessment])
// Handle start profiling from overview
const handleStartProfiling = useCallback(() => {
setActiveTab('wizard')
}, [])
// Handle reset
const handleReset = useCallback(() => {
const emptyState = createEmptyScopeState()
setScopeState(emptyState)
setActiveTab('overview')
localStorage.removeItem(STORAGE_KEY)
}, [])
// Calculate completion statistics
const completionStats = useMemo(() => {
const allQuestions = getAllQuestions()
const requiredQuestions = allQuestions.filter(q => q.required)
const totalQuestions = requiredQuestions.length
const answeredIds = new Set(scopeState.answers.map(a => a.questionId))
const answeredQuestions = requiredQuestions.filter(q => answeredIds.has(q.id)).length
const completionPercentage = totalQuestions > 0
? Math.round((answeredQuestions / totalQuestions) * 100)
: 0
const isComplete = answeredQuestions === totalQuestions
return {
total: totalQuestions,
answered: answeredQuestions,
percentage: completionPercentage,
isComplete,
}
}, [scopeState.answers])
// Auto-enable evaluation when all questions are answered
const canEvaluate = useMemo(() => {
return completionStats.isComplete
}, [completionStats.isComplete])
if (isLoading) {
return (
<div className="max-w-6xl mx-auto p-6">
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
)
}
return (
<div className="max-w-6xl mx-auto space-y-6 p-6">
{/* Step Header */}
<StepHeader
stepId="compliance-scope"
title="Compliance Scope Engine"
description="Umfang und Tiefe Ihrer Compliance-Dokumentation bestimmen"
explanation="Die Scope Engine analysiert Ihr Unternehmen anhand von 35 Fragen in 6 Bereichen und bestimmt deterministisch, welche Dokumente in welcher Tiefe benoetigt werden. Das 4-Level-Modell reicht von L1 (Lean Startup) bis L4 (Zertifizierungsbereit). Hard Triggers wie Art. 9 Daten oder Minderjährige heben das Level automatisch an."
tips={[
{
icon: 'lightbulb',
title: 'Deterministisch',
description: 'Alle Entscheidungen sind nachvollziehbar — keine KI, keine Black Box. Jede Empfehlung hat eine auditfähige Begründung.'
},
{
icon: 'info',
title: '4-Level-Modell',
description: 'L1 (Lean Startup) bis L4 (Zertifizierungsbereit). Das Level bestimmt Dokumentationstiefe und -umfang.'
},
{
icon: 'warning',
title: 'Hard Triggers',
description: 'Besondere Datenkategorien (Art. 9), Minderjährige oder Zertifizierungsziele heben das Level automatisch an.'
},
]}
/>
{/* Progress Indicator */}
{completionStats.answered > 0 && (
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="text-sm font-medium text-purple-900">
Fortschritt: {completionStats.answered} von {completionStats.total} Fragen beantwortet
</div>
<div className="text-sm font-semibold text-purple-700">
{completionStats.percentage}%
</div>
</div>
<div className="w-full bg-purple-200 rounded-full h-2">
<div
className="bg-purple-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${completionStats.percentage}%` }}
/>
</div>
</div>
)}
{/* Main Content Card */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm">
{/* Tab Navigation */}
<div className="border-b border-gray-200 px-6">
<nav className="flex gap-6 -mb-px">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors
${activeTab === tab.id
? 'border-purple-600 text-purple-700'
: 'border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300'
}
`}
>
<span className="text-lg">{tab.icon}</span>
<span>{tab.label}</span>
{tab.id === 'wizard' && completionStats.answered > 0 && (
<span className="ml-1 px-2 py-0.5 text-xs rounded-full bg-purple-100 text-purple-700">
{completionStats.percentage}%
</span>
)}
{tab.id === 'decision' && scopeState.decision && (
<span className="ml-1 w-2 h-2 rounded-full bg-green-500" />
)}
</button>
))}
</nav>
</div>
{/* Tab Content — pb-28 verhindert dass sticky Footer die unteren Buttons verdeckt */}
<div className="p-6 pb-28">
{activeTab === 'overview' && (
<ScopeOverviewTab
scopeState={scopeState}
completionStats={completionStats}
onStartProfiling={handleStartProfiling}
onReset={handleReset}
onGoToWizard={() => setActiveTab('wizard')}
onGoToDecision={() => setActiveTab('decision')}
onGoToExport={() => setActiveTab('export')}
/>
)}
{activeTab === 'wizard' && (
<ScopeWizardTab
answers={scopeState.answers}
onAnswersChange={handleAnswersChange}
onEvaluate={handleEvaluate}
canEvaluate={canEvaluate}
isEvaluating={isEvaluating}
completionStats={completionStats}
/>
)}
{activeTab === 'decision' && (
<ScopeDecisionTab
decision={scopeState.decision}
answers={scopeState.answers}
onBackToWizard={() => setActiveTab('wizard')}
onGoToExport={() => setActiveTab('export')}
canEvaluate={canEvaluate}
onEvaluate={handleEvaluate}
isEvaluating={isEvaluating}
applicableRegulations={applicableRegulations}
supervisoryAuthorities={supervisoryAuthorities}
regulationAssessmentLoading={regulationAssessmentLoading}
onGoToObligations={() => { window.location.href = '/sdk/obligations' }}
/>
)}
{activeTab === 'export' && (
<ScopeExportTab
scopeState={scopeState}
onBackToDecision={() => setActiveTab('decision')}
/>
)}
</div>
</div>
{/* Quick Action Buttons (Fixed at bottom on mobile) */}
<div className="sticky bottom-6 flex justify-between items-center gap-4 bg-white rounded-lg border border-gray-200 p-4 shadow-lg">
<div className="flex items-center gap-3">
<div className="text-sm text-gray-600">
{completionStats.isComplete ? (
<span className="flex items-center gap-2 text-green-700">
<span className="text-lg"></span>
<span className="font-medium">Profiling abgeschlossen</span>
</span>
) : (
<span className="flex items-center gap-2">
<span className="text-lg">📋</span>
<span>
{completionStats.answered === 0
? 'Starten Sie mit dem Profiling'
: `Noch ${completionStats.total - completionStats.answered} Fragen offen`
}
</span>
</span>
)}
</div>
</div>
<div className="flex items-center gap-3">
{activeTab !== 'wizard' && completionStats.answered > 0 && (
<button
onClick={() => setActiveTab('wizard')}
className="px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors"
>
Zum Fragebogen
</button>
)}
{canEvaluate && activeTab !== 'decision' && (
<button
onClick={handleEvaluate}
disabled={isEvaluating}
className="px-4 py-2 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isEvaluating ? 'Evaluiere...' : 'Scope evaluieren'}
</button>
)}
{scopeState.decision && activeTab !== 'export' && (
<button
onClick={() => setActiveTab('export')}
className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
>
Exportieren
</button>
)}
</div>
</div>
{/* Debug Info (only in development) */}
{process.env.NODE_ENV === 'development' && (
<details className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<summary className="cursor-pointer text-sm font-medium text-gray-700">
Debug Information
</summary>
<div className="mt-3 space-y-2 text-xs font-mono">
<div>
<span className="font-semibold">Active Tab:</span> {activeTab}
</div>
<div>
<span className="font-semibold">Total Answers:</span> {scopeState.answers.length}
</div>
<div>
<span className="font-semibold">Answered:</span> {completionStats.answered} ({completionStats.percentage}%)
</div>
<div>
<span className="font-semibold">Has Decision:</span> {scopeState.decision ? 'Yes' : 'No'}
</div>
{scopeState.decision && (
<>
<div>
<span className="font-semibold">Level:</span> {scopeState.decision.level}
</div>
<div>
<span className="font-semibold">Score:</span> {scopeState.decision.score}
</div>
<div>
<span className="font-semibold">Hard Triggers:</span> {scopeState.decision.hardTriggers.length}
</div>
</>
)}
<div>
<span className="font-semibold">Last Modified:</span> {scopeState.lastModified || 'Never'}
</div>
<div>
<span className="font-semibold">Can Evaluate:</span> {canEvaluate ? 'Yes' : 'No'}
</div>
</div>
</details>
)}
</div>
)
}