feat(regulations): Automatische Ableitung anwendbarer Gesetze & Aufsichtsbehoerden
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
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
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>
This commit is contained in:
@@ -12,7 +12,9 @@ import {
|
||||
import type {
|
||||
ComplianceScopeState,
|
||||
ScopeProfilingAnswer,
|
||||
ScopeDecision
|
||||
ScopeDecision,
|
||||
ApplicableRegulation,
|
||||
SupervisoryAuthorityInfo
|
||||
} from '@/lib/sdk/compliance-scope-types'
|
||||
import {
|
||||
createEmptyScopeState,
|
||||
@@ -20,6 +22,8 @@ import {
|
||||
} 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'
|
||||
|
||||
@@ -49,6 +53,11 @@ export default function ComplianceScopePage() {
|
||||
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(() => {
|
||||
@@ -75,6 +84,14 @@ export default function ComplianceScopePage() {
|
||||
// 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) {
|
||||
@@ -96,6 +113,41 @@ export default function ComplianceScopePage() {
|
||||
}))
|
||||
}, [])
|
||||
|
||||
// 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)
|
||||
@@ -112,13 +164,15 @@ export default function ComplianceScopePage() {
|
||||
|
||||
// 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)
|
||||
// Optionally show error toast/notification
|
||||
} finally {
|
||||
setIsEvaluating(false)
|
||||
}
|
||||
}, [scopeState.answers])
|
||||
}, [scopeState.answers, fetchRegulationAssessment])
|
||||
|
||||
// Handle start profiling from overview
|
||||
const handleStartProfiling = useCallback(() => {
|
||||
@@ -283,6 +337,10 @@ export default function ComplianceScopePage() {
|
||||
canEvaluate={canEvaluate}
|
||||
onEvaluate={handleEvaluate}
|
||||
isEvaluating={isEvaluating}
|
||||
applicableRegulations={applicableRegulations}
|
||||
supervisoryAuthorities={supervisoryAuthorities}
|
||||
regulationAssessmentLoading={regulationAssessmentLoading}
|
||||
onGoToObligations={() => { window.location.href = '/sdk/obligations' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
||||
import TOMControlPanel from '@/components/sdk/obligations/TOMControlPanel'
|
||||
import GapAnalysisView from '@/components/sdk/obligations/GapAnalysisView'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import { buildAssessmentPayload } from '@/lib/sdk/scope-to-facts'
|
||||
import type { ApplicableRegulation } from '@/lib/sdk/compliance-scope-types'
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
@@ -545,6 +548,7 @@ const REGULATION_CHIPS = [
|
||||
const UCCA_API = '/api/sdk/v1/ucca/obligations'
|
||||
|
||||
export default function ObligationsPage() {
|
||||
const { state: sdkState } = useSDK()
|
||||
const [obligations, setObligations] = useState<Obligation[]>([])
|
||||
const [stats, setStats] = useState<ObligationStats | null>(null)
|
||||
const [filter, setFilter] = useState('all')
|
||||
@@ -557,6 +561,7 @@ export default function ObligationsPage() {
|
||||
const [detailObligation, setDetailObligation] = useState<Obligation | null>(null)
|
||||
const [showGapAnalysis, setShowGapAnalysis] = useState(false)
|
||||
const [profiling, setProfiling] = useState(false)
|
||||
const [applicableRegs, setApplicableRegs] = useState<ApplicableRegulation[]>([])
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -667,26 +672,44 @@ export default function ObligationsPage() {
|
||||
setProfiling(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${UCCA_API}/assess-from-scope`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
// Build payload from real CompanyProfile + Scope data
|
||||
const profile = sdkState.companyProfile
|
||||
const scopeState = sdkState.complianceScope
|
||||
const scopeAnswers = scopeState?.answers || []
|
||||
const scopeDecision = scopeState?.decision || null
|
||||
|
||||
let payload: Record<string, unknown>
|
||||
if (profile) {
|
||||
payload = buildAssessmentPayload(profile, scopeAnswers, scopeDecision) as unknown as Record<string, unknown>
|
||||
} else {
|
||||
// Fallback: Minimaldaten wenn kein Profil vorhanden
|
||||
payload = {
|
||||
employee_count: 50,
|
||||
industry: 'technology',
|
||||
country: 'DE',
|
||||
processes_personal_data: true,
|
||||
is_controller: true,
|
||||
uses_processors: true,
|
||||
processes_special_categories: false,
|
||||
cross_border_transfer: true,
|
||||
uses_ai: true,
|
||||
determined_level: 'L2',
|
||||
}),
|
||||
determined_level: scopeDecision?.determinedLevel || 'L2',
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(`${UCCA_API}/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()
|
||||
if (data.obligations?.length > 0) {
|
||||
// Merge auto-profiled obligations into the view
|
||||
const autoObls: Obligation[] = data.obligations.map((o: Record<string, unknown>) => ({
|
||||
|
||||
// Store applicable regulations for the info box
|
||||
const regs: ApplicableRegulation[] = data.overview?.applicable_regulations || data.applicable_regulations || []
|
||||
setApplicableRegs(regs)
|
||||
|
||||
// Extract obligations from response (can be nested under overview)
|
||||
const rawObls = data.overview?.obligations || data.obligations || []
|
||||
if (rawObls.length > 0) {
|
||||
const autoObls: Obligation[] = rawObls.map((o: Record<string, unknown>) => ({
|
||||
id: o.id as string,
|
||||
title: o.title as string,
|
||||
description: (o.description as string) || '',
|
||||
@@ -818,6 +841,36 @@ export default function ObligationsPage() {
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-700">{error}</div>
|
||||
)}
|
||||
|
||||
{/* Applicable Regulations Info */}
|
||||
{applicableRegs.length > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-blue-900 mb-2">Anwendbare Regulierungen (aus Auto-Profiling)</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{applicableRegs.map(reg => (
|
||||
<span
|
||||
key={reg.id}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-white border border-blue-300 text-blue-800"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{reg.name}
|
||||
{reg.classification && <span className="text-blue-500">({reg.classification})</span>}
|
||||
<span className="text-blue-400">{reg.obligation_count} Pflichten</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No Profile Warning */}
|
||||
{!sdkState.companyProfile && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-700">
|
||||
Kein Unternehmensprofil vorhanden. Auto-Profiling verwendet Beispieldaten.{' '}
|
||||
<a href="/sdk/company-profile" className="underline font-medium">Profil anlegen →</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
|
||||
Reference in New Issue
Block a user