'use client' import React, { useState, useEffect, useCallback, useMemo, useRef } 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() // Project-specific storage key const projectStorageKey = sdkState.projectId ? `${STORAGE_KEY}_${sdkState.projectId}` : STORAGE_KEY // Active tab state const [activeTab, setActiveTab] = useState('overview') // Migrate old decision format: drop decision if it has old-format fields const migrateState = (state: ComplianceScopeState): ComplianceScopeState => { if (state.decision) { const d = state.decision as Record // Old format had 'level' instead of 'determinedLevel', or docs with 'isMandatory' if (d.level || !d.determinedLevel) { return { ...state, decision: null } } } return state } // Local scope state const [scopeState, setScopeState] = useState(() => { // Try to load from SDK context first if (sdkState.complianceScope) { return migrateState(sdkState.complianceScope) } return createEmptyScopeState() }) // Loading state const [isLoading, setIsLoading] = useState(true) const [isEvaluating, setIsEvaluating] = useState(false) // Guard against save-loop: tracks whether we're syncing FROM SDK → local state const syncingFromSdk = useRef(false) // Regulation assessment state const [applicableRegulations, setApplicableRegulations] = useState([]) const [supervisoryAuthorities, setSupervisoryAuthorities] = useState([]) const [regulationAssessmentLoading, setRegulationAssessmentLoading] = useState(false) // Sync from SDK context when it becomes available (handles async loading). // The SDK context loads state from server/localStorage asynchronously, so // sdkState.complianceScope may arrive AFTER this page has already mounted. useEffect(() => { const ctxScope = sdkState.complianceScope if (ctxScope && ctxScope.answers?.length > 0) { syncingFromSdk.current = true setScopeState(migrateState(ctxScope)) setIsLoading(false) } else if (isLoading) { // SDK has no scope data — try localStorage fallback, then give up try { const stored = localStorage.getItem(projectStorageKey) if (stored) { const parsed = migrateState(JSON.parse(stored) as ComplianceScopeState) if (parsed.answers?.length > 0) { setScopeState(parsed) dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed }) } } } catch (error) { console.error('Failed to load compliance scope from localStorage:', error) } setIsLoading(false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [sdkState.complianceScope]) // 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 LOCAL state changes (user edits). // Guarded: don't save empty state, and don't echo back what we just loaded from SDK. useEffect(() => { if (!isLoading && scopeState.answers.length > 0) { if (syncingFromSdk.current) { syncingFromSdk.current = false return } try { localStorage.setItem(projectStorageKey, 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(projectStorageKey) }, []) // 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]) // Mark sidebar step as complete when all required questions answered AND decision exists useEffect(() => { if (completionStats.isComplete && scopeState.decision) { dispatch({ type: 'COMPLETE_STEP', payload: 'compliance-scope' }) } }, [completionStats.isComplete, scopeState.decision, dispatch]) if (isLoading) { return (
Loading...
) } return (
{/* Step Header */} {/* Progress Indicator */} {completionStats.answered > 0 && (
Fortschritt: {completionStats.answered} von {completionStats.total} Fragen beantwortet
{completionStats.percentage}%
)} {/* Main Content Card */}
{/* Tab Navigation */}
{/* Tab Content — pb-28 verhindert dass sticky Footer die unteren Buttons verdeckt */}
{activeTab === 'overview' && ( setActiveTab('wizard')} onGoToDecision={() => setActiveTab('decision')} onGoToExport={() => setActiveTab('export')} /> )} {activeTab === 'wizard' && ( )} {activeTab === 'decision' && ( 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' && ( setActiveTab('decision')} /> )}
{/* Quick Action Buttons (Fixed at bottom on mobile) */}
{completionStats.isComplete ? ( âś“ Profiling abgeschlossen ) : ( đź“‹ {completionStats.answered === 0 ? 'Starten Sie mit dem Profiling' : `Noch ${completionStats.total - completionStats.answered} Fragen offen` } )}
{activeTab !== 'wizard' && completionStats.answered > 0 && ( )} {canEvaluate && activeTab !== 'decision' && ( )} {scopeState.decision && activeTab !== 'export' && ( )}
{/* Debug Info (only in development) */} {process.env.NODE_ENV === 'development' && (
Debug Information
Active Tab: {activeTab}
Total Answers: {scopeState.answers.length}
Answered: {completionStats.answered} ({completionStats.percentage}%)
Has Decision: {scopeState.decision ? 'Yes' : 'No'}
{scopeState.decision && ( <>
Level: {scopeState.decision.determinedLevel}
Score: {scopeState.decision.scores?.composite_score}
Hard Triggers: {scopeState.decision.triggeredHardTriggers.length}
)}
Last Modified: {scopeState.lastModified || 'Never'}
Can Evaluate: {canEvaluate ? 'Yes' : 'No'}
)}
) }