diff --git a/admin-compliance/lib/sdk/context-hooks.ts b/admin-compliance/lib/sdk/context-hooks.ts new file mode 100644 index 0000000..04b9cba --- /dev/null +++ b/admin-compliance/lib/sdk/context-hooks.ts @@ -0,0 +1,17 @@ +'use client' + +import { useContext } from 'react' +import { SDKContextValue } from './context-types' +import { SDKContext } from './context-provider' + +// ============================================================================= +// HOOK +// ============================================================================= + +export function useSDK(): SDKContextValue { + const context = useContext(SDKContext) + if (!context) { + throw new Error('useSDK must be used within SDKProvider') + } + return context +} diff --git a/admin-compliance/lib/sdk/context-projects.ts b/admin-compliance/lib/sdk/context-projects.ts new file mode 100644 index 0000000..ecdfc90 --- /dev/null +++ b/admin-compliance/lib/sdk/context-projects.ts @@ -0,0 +1,67 @@ +import React from 'react' +import { SDKApiClient, getSDKApiClient } from './api-client' +import { CustomerType, ProjectInfo } from './types' + +// ============================================================================= +// PROJECT MANAGEMENT HELPERS +// ============================================================================= + +/** + * Ensures an API client is available. If the ref is null and backend sync is + * enabled, lazily initialises one. Returns the client or throws. + */ +export function ensureApiClient( + apiClientRef: React.MutableRefObject, + enableBackendSync: boolean, + tenantId: string, + projectId?: string +): SDKApiClient { + if (!apiClientRef.current && enableBackendSync) { + apiClientRef.current = getSDKApiClient(tenantId, projectId) + } + if (!apiClientRef.current) { + throw new Error('Backend sync not enabled') + } + return apiClientRef.current +} + +export async function createProjectApi( + apiClient: SDKApiClient, + name: string, + customerType: CustomerType, + copyFromProjectId?: string +): Promise { + return apiClient.createProject({ + name, + customer_type: customerType, + copy_from_project_id: copyFromProjectId, + }) +} + +export async function listProjectsApi( + apiClient: SDKApiClient +): Promise { + const result = await apiClient.listProjects() + return result.projects +} + +export async function archiveProjectApi( + apiClient: SDKApiClient, + archiveId: string +): Promise { + await apiClient.archiveProject(archiveId) +} + +export async function restoreProjectApi( + apiClient: SDKApiClient, + restoreId: string +): Promise { + return apiClient.restoreProject(restoreId) +} + +export async function permanentlyDeleteProjectApi( + apiClient: SDKApiClient, + deleteId: string +): Promise { + await apiClient.permanentlyDeleteProject(deleteId) +} diff --git a/admin-compliance/lib/sdk/context-provider.tsx b/admin-compliance/lib/sdk/context-provider.tsx new file mode 100644 index 0000000..c6f0fab --- /dev/null +++ b/admin-compliance/lib/sdk/context-provider.tsx @@ -0,0 +1,495 @@ +'use client' + +import React, { createContext, useReducer, useEffect, useCallback, useMemo, useRef } from 'react' +import { useRouter, usePathname } from 'next/navigation' +import { + SDKState, + CheckpointStatus, + UseCaseAssessment, + Risk, + Control, + CustomerType, + CompanyProfile, + ImportedDocument, + GapAnalysis, + SDKPackageId, + ProjectInfo, + getStepById, + getStepByUrl, + getNextStep, + getPreviousStep, + getCompletionPercentage, + getPhaseCompletionPercentage, + getPackageCompletionPercentage, +} from './types' +import { exportToPDF, exportToZIP } from './export' +import { SDKApiClient, getSDKApiClient } from './api-client' +import { StateSyncManager, SyncState } from './sync' +import { generateDemoState } from './demo-data' +import { SDKContextValue, initialState, SDK_STORAGE_KEY } from './context-types' +import { sdkReducer } from './context-reducer' +import { validateCheckpointLocally } from './context-validators' +import { + ensureApiClient, + createProjectApi, + listProjectsApi, + archiveProjectApi, + restoreProjectApi, + permanentlyDeleteProjectApi, +} from './context-projects' +import { + buildSyncCallbacks, + loadInitialState, + initSyncInfra, + cleanupSyncInfra, +} from './context-sync-helpers' + +export const SDKContext = createContext(null) + +interface SDKProviderProps { + children: React.ReactNode + tenantId?: string + userId?: string + projectId?: string + enableBackendSync?: boolean +} + +export function SDKProvider({ + children, + tenantId = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e', + userId = 'default', + projectId, + enableBackendSync = false, +}: SDKProviderProps) { + const router = useRouter() + const pathname = usePathname() + const [state, dispatch] = useReducer(sdkReducer, { + ...initialState, + tenantId, + userId, + projectId: projectId || '', + }) + const [isCommandBarOpen, setCommandBarOpen] = React.useState(false) + const [isInitialized, setIsInitialized] = React.useState(false) + const [syncState, setSyncState] = React.useState({ + status: 'idle', + lastSyncedAt: null, + localVersion: 0, + serverVersion: 0, + pendingChanges: 0, + error: null, + }) + const [isOnline, setIsOnline] = React.useState(true) + + // Refs for sync manager and API client + const apiClientRef = useRef(null) + const syncManagerRef = useRef(null) + const stateRef = useRef(state) + stateRef.current = state + + // Initialize API client and sync manager + useEffect(() => { + const callbacks = buildSyncCallbacks(setSyncState, setIsOnline, dispatch, stateRef) + initSyncInfra(enableBackendSync, tenantId, projectId, apiClientRef, syncManagerRef, callbacks) + return () => cleanupSyncInfra(enableBackendSync, syncManagerRef, apiClientRef) + }, [enableBackendSync, tenantId, projectId]) + + // Sync current step with URL + useEffect(() => { + if (pathname) { + const step = getStepByUrl(pathname) + if (step && step.id !== state.currentStep) { + dispatch({ type: 'SET_CURRENT_STEP', payload: step.id }) + } + } + }, [pathname, state.currentStep]) + + // Storage key — per tenant+project + const storageKey = projectId + ? `${SDK_STORAGE_KEY}-${tenantId}-${projectId}` + : `${SDK_STORAGE_KEY}-${tenantId}` + + // Load state on mount (localStorage first, then server) + useEffect(() => { + loadInitialState({ + storageKey, + enableBackendSync, + projectId, + syncManager: syncManagerRef.current, + apiClient: apiClientRef.current, + dispatch, + }) + .catch(error => console.error('Failed to load SDK state:', error)) + .finally(() => setIsInitialized(true)) + }, [tenantId, projectId, enableBackendSync, storageKey]) + + // Auto-save to localStorage and sync to server + useEffect(() => { + if (!isInitialized || !state.preferences.autoSave) return + + const saveTimeout = setTimeout(() => { + try { + // Save to localStorage (per tenant+project) + localStorage.setItem(storageKey, JSON.stringify(state)) + + // Sync to server if backend sync is enabled + if (enableBackendSync && syncManagerRef.current) { + syncManagerRef.current.queueSync(state) + } + } catch (error) { + console.error('Failed to save SDK state:', error) + } + }, 1000) + + return () => clearTimeout(saveTimeout) + }, [state, tenantId, projectId, isInitialized, enableBackendSync, storageKey]) + + // Keyboard shortcut for Command Bar + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault() + setCommandBarOpen(prev => !prev) + } + if (e.key === 'Escape' && isCommandBarOpen) { + setCommandBarOpen(false) + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [isCommandBarOpen]) + + // Navigation + const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep]) + + const goToStep = useCallback( + (stepId: string) => { + const step = getStepById(stepId) + if (step) { + dispatch({ type: 'SET_CURRENT_STEP', payload: stepId }) + const url = projectId ? `${step.url}?project=${projectId}` : step.url + router.push(url) + } + }, + [router, projectId] + ) + + const goToNextStep = useCallback(() => { + const nextStep = getNextStep(state.currentStep, state) + if (nextStep) { + goToStep(nextStep.id) + } + }, [state, goToStep]) + + const goToPreviousStep = useCallback(() => { + const prevStep = getPreviousStep(state.currentStep, state) + if (prevStep) { + goToStep(prevStep.id) + } + }, [state, goToStep]) + + const canGoNext = useMemo(() => { + return getNextStep(state.currentStep, state) !== undefined + }, [state]) + + const canGoPrevious = useMemo(() => { + return getPreviousStep(state.currentStep, state) !== undefined + }, [state]) + + // Progress + const completionPercentage = useMemo(() => getCompletionPercentage(state), [state]) + const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state]) + const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state]) + + // Package Completion + const packageCompletion = useMemo(() => { + const completion: Record = { + 'vorbereitung': getPackageCompletionPercentage(state, 'vorbereitung'), + 'analyse': getPackageCompletionPercentage(state, 'analyse'), + 'dokumentation': getPackageCompletionPercentage(state, 'dokumentation'), + 'rechtliche-texte': getPackageCompletionPercentage(state, 'rechtliche-texte'), + 'betrieb': getPackageCompletionPercentage(state, 'betrieb'), + } + return completion + }, [state]) + + // Simple dispatch callbacks + const setCustomerType = useCallback((type: CustomerType) => dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type }), []) + const setCompanyProfile = useCallback((profile: CompanyProfile) => dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile }), []) + const updateCompanyProfile = useCallback((updates: Partial) => dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates }), []) + const setComplianceScope = useCallback((scope: import('./compliance-scope-types').ComplianceScopeState) => dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scope }), []) + const updateComplianceScope = useCallback((updates: Partial) => dispatch({ type: 'UPDATE_COMPLIANCE_SCOPE', payload: updates }), []) + const addImportedDocument = useCallback((doc: ImportedDocument) => dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc }), []) + const setGapAnalysis = useCallback((analysis: GapAnalysis) => dispatch({ type: 'SET_GAP_ANALYSIS', payload: analysis }), []) + + // Checkpoints + const validateCheckpoint = useCallback( + async (checkpointId: string): Promise => { + // Try backend validation if available + if (enableBackendSync && apiClientRef.current) { + try { + const result = await apiClientRef.current.validateCheckpoint(checkpointId, state) + const status: CheckpointStatus = { + checkpointId: result.checkpointId, + passed: result.passed, + validatedAt: new Date(result.validatedAt), + validatedBy: result.validatedBy, + errors: result.errors, + warnings: result.warnings, + } + dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } }) + return status + } catch { + // Fall back to local validation + } + } + + // Local validation + const status = validateCheckpointLocally(checkpointId, state) + + dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } }) + return status + }, + [state, enableBackendSync] + ) + + const overrideCheckpoint = useCallback(async (checkpointId: string, reason: string): Promise => { + const existing = state.checkpoints[checkpointId] + const overridden: CheckpointStatus = { + ...existing, checkpointId, passed: true, overrideReason: reason, + overriddenBy: state.userId, overriddenAt: new Date(), + errors: [], warnings: existing?.warnings || [], + } + dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overridden } }) + }, [state.checkpoints, state.userId]) + + const getCheckpointStatus = useCallback( + (checkpointId: string) => state.checkpoints[checkpointId], + [state.checkpoints] + ) + + const updateUseCase = useCallback((id: string, data: Partial) => dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } }), []) + const addRisk = useCallback((risk: Risk) => dispatch({ type: 'ADD_RISK', payload: risk }), []) + const updateControl = useCallback((id: string, data: Partial) => dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } }), []) + const loadDemoData = useCallback((demoState: Partial) => dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState }), []) + + // Seed demo data via API (stores like real customer data) + const seedDemoData = useCallback(async (): Promise<{ success: boolean; message: string }> => { + try { + // Generate demo state + const demoState = generateDemoState(tenantId, userId) as SDKState + + // Save via API (same path as real customer data) + if (enableBackendSync && apiClientRef.current) { + await apiClientRef.current.saveState(demoState) + } + + // Also save to localStorage for immediate availability + localStorage.setItem(storageKey, JSON.stringify(demoState)) + + // Update local state + dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState }) + + return { success: true, message: `Demo-Daten erfolgreich geladen für Tenant ${tenantId}` } + } catch (error) { + console.error('Failed to seed demo data:', error) + return { + success: false, + message: error instanceof Error ? error.message : 'Unbekannter Fehler beim Laden der Demo-Daten', + } + } + }, [tenantId, userId, enableBackendSync, storageKey]) + + // Clear demo data + const clearDemoData = useCallback(async (): Promise => { + try { + // Delete from API + if (enableBackendSync && apiClientRef.current) { + await apiClientRef.current.deleteState() + } + + // Clear localStorage + localStorage.removeItem(storageKey) + + // Reset local state + dispatch({ type: 'RESET_STATE' }) + + return true + } catch (error) { + console.error('Failed to clear demo data:', error) + return false + } + }, [storageKey, enableBackendSync]) + + // Check if demo data is loaded (has use cases with demo- prefix) + const isDemoDataLoaded = useMemo(() => { + return state.useCases.length > 0 && state.useCases.some(uc => uc.id.startsWith('demo-')) + }, [state.useCases]) + + // Persistence + const saveState = useCallback(async (): Promise => { + try { + localStorage.setItem(storageKey, JSON.stringify(state)) + + if (enableBackendSync && syncManagerRef.current) { + await syncManagerRef.current.forcSync(state) + } + } catch (error) { + console.error('Failed to save SDK state:', error) + throw error + } + }, [state, storageKey, enableBackendSync]) + + const loadState = useCallback(async (): Promise => { + try { + if (enableBackendSync && syncManagerRef.current) { + const serverState = await syncManagerRef.current.loadFromServer() + if (serverState) { + dispatch({ type: 'SET_STATE', payload: serverState }) + return + } + } + + // Fall back to localStorage + const stored = localStorage.getItem(storageKey) + if (stored) { + const parsed = JSON.parse(stored) + dispatch({ type: 'SET_STATE', payload: parsed }) + } + } catch (error) { + console.error('Failed to load SDK state:', error) + throw error + } + }, [storageKey, enableBackendSync]) + + // Force sync to server + const forceSyncToServer = useCallback(async (): Promise => { + if (enableBackendSync && syncManagerRef.current) { + await syncManagerRef.current.forcSync(state) + } + }, [state, enableBackendSync]) + + // Project Management + const createProject = useCallback( + async (name: string, customerType: CustomerType, copyFromProjectId?: string): Promise => { + const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) + return createProjectApi(client, name, customerType, copyFromProjectId) + }, + [enableBackendSync, tenantId, projectId] + ) + + const listProjectsFn = useCallback(async (): Promise => { + if (!apiClientRef.current && enableBackendSync) { + apiClientRef.current = getSDKApiClient(tenantId, projectId) + } + if (!apiClientRef.current) { + return [] + } + return listProjectsApi(apiClientRef.current) + }, [enableBackendSync, tenantId, projectId]) + + const switchProject = useCallback( + (newProjectId: string) => { + const params = new URLSearchParams(window.location.search) + params.set('project', newProjectId) + router.push(`/sdk?${params.toString()}`) + }, + [router] + ) + + const archiveProjectFn = useCallback( + async (archiveId: string): Promise => { + const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) + await archiveProjectApi(client, archiveId) + }, + [enableBackendSync, tenantId, projectId] + ) + + const restoreProjectFn = useCallback( + async (restoreId: string): Promise => { + const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) + return restoreProjectApi(client, restoreId) + }, + [enableBackendSync, tenantId, projectId] + ) + + const permanentlyDeleteProjectFn = useCallback( + async (deleteId: string): Promise => { + const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) + await permanentlyDeleteProjectApi(client, deleteId) + }, + [enableBackendSync, tenantId, projectId] + ) + + // Export + const exportState = useCallback( + async (format: 'json' | 'pdf' | 'zip'): Promise => { + switch (format) { + case 'json': + return new Blob([JSON.stringify(state, null, 2)], { + type: 'application/json', + }) + + case 'pdf': + return exportToPDF(state) + + case 'zip': + return exportToZIP(state) + + default: + throw new Error(`Unknown export format: ${format}`) + } + }, + [state] + ) + + const value: SDKContextValue = { + state, + dispatch, + currentStep, + goToStep, + goToNextStep, + goToPreviousStep, + canGoNext, + canGoPrevious, + completionPercentage, + phase1Completion, + phase2Completion, + packageCompletion, + setCustomerType, + setCompanyProfile, + updateCompanyProfile, + setComplianceScope, + updateComplianceScope, + addImportedDocument, + setGapAnalysis, + validateCheckpoint, + overrideCheckpoint, + getCheckpointStatus, + updateUseCase, + addRisk, + updateControl, + saveState, + loadState, + loadDemoData, + seedDemoData, + clearDemoData, + isDemoDataLoaded, + syncState, + forceSyncToServer, + isOnline, + exportState, + isCommandBarOpen, + setCommandBarOpen, + projectId, + createProject, + listProjects: listProjectsFn, + switchProject, + archiveProject: archiveProjectFn, + restoreProject: restoreProjectFn, + permanentlyDeleteProject: permanentlyDeleteProjectFn, + } + + return {children} +} diff --git a/admin-compliance/lib/sdk/context-reducer.ts b/admin-compliance/lib/sdk/context-reducer.ts new file mode 100644 index 0000000..ed98f1c --- /dev/null +++ b/admin-compliance/lib/sdk/context-reducer.ts @@ -0,0 +1,353 @@ +import { + SDKState, + getStepById, +} from './types' +import { ExtendedSDKAction, initialState } from './context-types' + +// ============================================================================= +// REDUCER +// ============================================================================= + +export function sdkReducer(state: SDKState, action: ExtendedSDKAction): SDKState { + const updateState = (updates: Partial): SDKState => ({ + ...state, + ...updates, + lastModified: new Date(), + }) + + switch (action.type) { + case 'SET_STATE': + return updateState(action.payload) + + case 'LOAD_DEMO_DATA': + // Load demo data while preserving user preferences + return { + ...initialState, + ...action.payload, + tenantId: state.tenantId, + userId: state.userId, + preferences: state.preferences, + lastModified: new Date(), + } + + case 'SET_CURRENT_STEP': { + const step = getStepById(action.payload) + return updateState({ + currentStep: action.payload, + currentPhase: step?.phase || state.currentPhase, + }) + } + + case 'COMPLETE_STEP': + if (state.completedSteps.includes(action.payload)) { + return state + } + return updateState({ + completedSteps: [...state.completedSteps, action.payload], + }) + + case 'SET_CHECKPOINT_STATUS': + return updateState({ + checkpoints: { + ...state.checkpoints, + [action.payload.id]: action.payload.status, + }, + }) + + case 'SET_CUSTOMER_TYPE': + return updateState({ customerType: action.payload }) + + case 'SET_COMPANY_PROFILE': + return updateState({ companyProfile: action.payload }) + + case 'UPDATE_COMPANY_PROFILE': + return updateState({ + companyProfile: state.companyProfile + ? { ...state.companyProfile, ...action.payload } + : null, + }) + + case 'SET_COMPLIANCE_SCOPE': + return updateState({ complianceScope: action.payload }) + + case 'UPDATE_COMPLIANCE_SCOPE': + return updateState({ + complianceScope: state.complianceScope + ? { ...state.complianceScope, ...action.payload } + : null, + }) + + case 'ADD_IMPORTED_DOCUMENT': + return updateState({ + importedDocuments: [...state.importedDocuments, action.payload], + }) + + case 'UPDATE_IMPORTED_DOCUMENT': + return updateState({ + importedDocuments: state.importedDocuments.map(doc => + doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc + ), + }) + + case 'DELETE_IMPORTED_DOCUMENT': + return updateState({ + importedDocuments: state.importedDocuments.filter(doc => doc.id !== action.payload), + }) + + case 'SET_GAP_ANALYSIS': + return updateState({ gapAnalysis: action.payload }) + + case 'ADD_USE_CASE': + return updateState({ + useCases: [...state.useCases, action.payload], + }) + + case 'UPDATE_USE_CASE': + return updateState({ + useCases: state.useCases.map(uc => + uc.id === action.payload.id ? { ...uc, ...action.payload.data } : uc + ), + }) + + case 'DELETE_USE_CASE': + return updateState({ + useCases: state.useCases.filter(uc => uc.id !== action.payload), + activeUseCase: state.activeUseCase === action.payload ? null : state.activeUseCase, + }) + + case 'SET_ACTIVE_USE_CASE': + return updateState({ activeUseCase: action.payload }) + + case 'SET_SCREENING': + return updateState({ screening: action.payload }) + + case 'ADD_MODULE': + return updateState({ + modules: [...state.modules, action.payload], + }) + + case 'UPDATE_MODULE': + return updateState({ + modules: state.modules.map(m => + m.id === action.payload.id ? { ...m, ...action.payload.data } : m + ), + }) + + case 'ADD_REQUIREMENT': + return updateState({ + requirements: [...state.requirements, action.payload], + }) + + case 'UPDATE_REQUIREMENT': + return updateState({ + requirements: state.requirements.map(r => + r.id === action.payload.id ? { ...r, ...action.payload.data } : r + ), + }) + + case 'ADD_CONTROL': + return updateState({ + controls: [...state.controls, action.payload], + }) + + case 'UPDATE_CONTROL': + return updateState({ + controls: state.controls.map(c => + c.id === action.payload.id ? { ...c, ...action.payload.data } : c + ), + }) + + case 'ADD_EVIDENCE': + return updateState({ + evidence: [...state.evidence, action.payload], + }) + + case 'UPDATE_EVIDENCE': + return updateState({ + evidence: state.evidence.map(e => + e.id === action.payload.id ? { ...e, ...action.payload.data } : e + ), + }) + + case 'DELETE_EVIDENCE': + return updateState({ + evidence: state.evidence.filter(e => e.id !== action.payload), + }) + + case 'ADD_RISK': + return updateState({ + risks: [...state.risks, action.payload], + }) + + case 'UPDATE_RISK': + return updateState({ + risks: state.risks.map(r => + r.id === action.payload.id ? { ...r, ...action.payload.data } : r + ), + }) + + case 'DELETE_RISK': + return updateState({ + risks: state.risks.filter(r => r.id !== action.payload), + }) + + case 'SET_AI_ACT_RESULT': + return updateState({ aiActClassification: action.payload }) + + case 'ADD_OBLIGATION': + return updateState({ + obligations: [...state.obligations, action.payload], + }) + + case 'UPDATE_OBLIGATION': + return updateState({ + obligations: state.obligations.map(o => + o.id === action.payload.id ? { ...o, ...action.payload.data } : o + ), + }) + + case 'SET_DSFA': + return updateState({ dsfa: action.payload }) + + case 'ADD_TOM': + return updateState({ + toms: [...state.toms, action.payload], + }) + + case 'UPDATE_TOM': + return updateState({ + toms: state.toms.map(t => + t.id === action.payload.id ? { ...t, ...action.payload.data } : t + ), + }) + + case 'ADD_RETENTION_POLICY': + return updateState({ + retentionPolicies: [...state.retentionPolicies, action.payload], + }) + + case 'UPDATE_RETENTION_POLICY': + return updateState({ + retentionPolicies: state.retentionPolicies.map(p => + p.id === action.payload.id ? { ...p, ...action.payload.data } : p + ), + }) + + case 'ADD_PROCESSING_ACTIVITY': + return updateState({ + vvt: [...state.vvt, action.payload], + }) + + case 'UPDATE_PROCESSING_ACTIVITY': + return updateState({ + vvt: state.vvt.map(p => + p.id === action.payload.id ? { ...p, ...action.payload.data } : p + ), + }) + + case 'ADD_DOCUMENT': + return updateState({ + documents: [...state.documents, action.payload], + }) + + case 'UPDATE_DOCUMENT': + return updateState({ + documents: state.documents.map(d => + d.id === action.payload.id ? { ...d, ...action.payload.data } : d + ), + }) + + case 'SET_COOKIE_BANNER': + return updateState({ cookieBanner: action.payload }) + + case 'SET_DSR_CONFIG': + return updateState({ dsrConfig: action.payload }) + + case 'ADD_ESCALATION_WORKFLOW': + return updateState({ + escalationWorkflows: [...state.escalationWorkflows, action.payload], + }) + + case 'UPDATE_ESCALATION_WORKFLOW': + return updateState({ + escalationWorkflows: state.escalationWorkflows.map(w => + w.id === action.payload.id ? { ...w, ...action.payload.data } : w + ), + }) + + case 'ADD_SECURITY_ISSUE': + return updateState({ + securityIssues: [...state.securityIssues, action.payload], + }) + + case 'UPDATE_SECURITY_ISSUE': + return updateState({ + securityIssues: state.securityIssues.map(i => + i.id === action.payload.id ? { ...i, ...action.payload.data } : i + ), + }) + + case 'ADD_BACKLOG_ITEM': + return updateState({ + securityBacklog: [...state.securityBacklog, action.payload], + }) + + case 'UPDATE_BACKLOG_ITEM': + return updateState({ + securityBacklog: state.securityBacklog.map(i => + i.id === action.payload.id ? { ...i, ...action.payload.data } : i + ), + }) + + case 'ADD_COMMAND_HISTORY': + return updateState({ + commandBarHistory: [action.payload, ...state.commandBarHistory].slice(0, 50), + }) + + case 'SET_PREFERENCES': + return updateState({ + preferences: { ...state.preferences, ...action.payload }, + }) + + case 'ADD_CUSTOM_CATALOG_ENTRY': { + const entry = action.payload + const existing = state.customCatalogs[entry.catalogId] || [] + return updateState({ + customCatalogs: { + ...state.customCatalogs, + [entry.catalogId]: [...existing, entry], + }, + }) + } + + case 'UPDATE_CUSTOM_CATALOG_ENTRY': { + const { catalogId, entryId, data } = action.payload + const entries = state.customCatalogs[catalogId] || [] + return updateState({ + customCatalogs: { + ...state.customCatalogs, + [catalogId]: entries.map(e => + e.id === entryId ? { ...e, data: { ...e.data, ...data }, updatedAt: new Date().toISOString() } : e + ), + }, + }) + } + + case 'DELETE_CUSTOM_CATALOG_ENTRY': { + const { catalogId, entryId } = action.payload + const items = state.customCatalogs[catalogId] || [] + return updateState({ + customCatalogs: { + ...state.customCatalogs, + [catalogId]: items.filter(e => e.id !== entryId), + }, + }) + } + + case 'RESET_STATE': + return { ...initialState, lastModified: new Date() } + + default: + return state + } +} diff --git a/admin-compliance/lib/sdk/context-sync-helpers.ts b/admin-compliance/lib/sdk/context-sync-helpers.ts new file mode 100644 index 0000000..4b743d2 --- /dev/null +++ b/admin-compliance/lib/sdk/context-sync-helpers.ts @@ -0,0 +1,145 @@ +import React from 'react' +import { SDKState } from './types' +import { SDKApiClient, getSDKApiClient, resetSDKApiClient } from './api-client' +import { StateSyncManager, createStateSyncManager, SyncState, SyncCallbacks } from './sync' +import { ExtendedSDKAction } from './context-types' + +// ============================================================================= +// SYNC CALLBACK BUILDER +// ============================================================================= + +/** + * Builds the SyncCallbacks object used by the StateSyncManager. + * Keeps the provider component cleaner by extracting this factory. + */ +export function buildSyncCallbacks( + setSyncState: React.Dispatch>, + setIsOnline: React.Dispatch>, + dispatch: React.Dispatch, + stateRef: React.MutableRefObject +): SyncCallbacks { + return { + onSyncStart: () => { + setSyncState(prev => ({ ...prev, status: 'syncing' })) + }, + onSyncComplete: (syncedState) => { + setSyncState(prev => ({ + ...prev, + status: 'idle', + lastSyncedAt: new Date(), + pendingChanges: 0, + })) + if (syncedState.lastModified > stateRef.current.lastModified) { + dispatch({ type: 'SET_STATE', payload: syncedState }) + } + }, + onSyncError: (error) => { + setSyncState(prev => ({ + ...prev, + status: 'error', + error: error.message, + })) + }, + onConflict: () => { + setSyncState(prev => ({ ...prev, status: 'conflict' })) + }, + onOffline: () => { + setIsOnline(false) + setSyncState(prev => ({ ...prev, status: 'offline' })) + }, + onOnline: () => { + setIsOnline(true) + setSyncState(prev => ({ ...prev, status: 'idle' })) + }, + } +} + +// ============================================================================= +// INITIAL STATE LOADER +// ============================================================================= + +/** + * Loads SDK state from localStorage and optionally from the server, + * dispatching SET_STATE as appropriate. + */ +export async function loadInitialState(params: { + storageKey: string + enableBackendSync: boolean + projectId?: string + syncManager: StateSyncManager | null + apiClient: SDKApiClient | null + dispatch: React.Dispatch +}): Promise { + const { storageKey, enableBackendSync, projectId, syncManager, apiClient, dispatch } = params + + // First, try loading from localStorage + const stored = localStorage.getItem(storageKey) + if (stored) { + const parsed = JSON.parse(stored) + if (parsed.lastModified) { + parsed.lastModified = new Date(parsed.lastModified) + } + dispatch({ type: 'SET_STATE', payload: parsed }) + } + + // Then, try loading from server if backend sync is enabled + if (enableBackendSync && syncManager) { + const serverState = await syncManager.loadFromServer() + if (serverState) { + const localTime = stored ? new Date(JSON.parse(stored).lastModified).getTime() : 0 + const serverTime = new Date(serverState.lastModified).getTime() + if (serverTime > localTime) { + dispatch({ type: 'SET_STATE', payload: serverState }) + } + } + } + + // Load project metadata (name, status, etc.) from backend + if (enableBackendSync && projectId && apiClient) { + try { + const info = await apiClient.getProject(projectId) + dispatch({ type: 'SET_STATE', payload: { projectInfo: info } }) + } catch (err) { + console.warn('Failed to load project info:', err) + } + } +} + +// ============================================================================= +// INIT / CLEANUP HELPERS +// ============================================================================= + +export function initSyncInfra( + enableBackendSync: boolean, + tenantId: string, + projectId: string | undefined, + apiClientRef: React.MutableRefObject, + syncManagerRef: React.MutableRefObject, + callbacks: SyncCallbacks +): void { + if (!enableBackendSync || typeof window === 'undefined') return + + apiClientRef.current = getSDKApiClient(tenantId, projectId) + syncManagerRef.current = createStateSyncManager( + apiClientRef.current, + tenantId, + { debounceMs: 2000, maxRetries: 3 }, + callbacks, + projectId + ) +} + +export function cleanupSyncInfra( + enableBackendSync: boolean, + syncManagerRef: React.MutableRefObject, + apiClientRef: React.MutableRefObject +): void { + if (syncManagerRef.current) { + syncManagerRef.current.destroy() + syncManagerRef.current = null + } + if (enableBackendSync) { + resetSDKApiClient() + apiClientRef.current = null + } +} diff --git a/admin-compliance/lib/sdk/context-types.ts b/admin-compliance/lib/sdk/context-types.ts new file mode 100644 index 0000000..0290b75 --- /dev/null +++ b/admin-compliance/lib/sdk/context-types.ts @@ -0,0 +1,203 @@ +import React from 'react' +import { + SDKState, + SDKAction, + SDKStep, + CheckpointStatus, + UseCaseAssessment, + Risk, + Control, + UserPreferences, + CustomerType, + CompanyProfile, + ImportedDocument, + GapAnalysis, + SDKPackageId, + ProjectInfo, +} from './types' +import { SyncState } from './sync' + +// ============================================================================= +// INITIAL STATE +// ============================================================================= + +const initialPreferences: UserPreferences = { + language: 'de', + theme: 'light', + compactMode: false, + showHints: true, + autoSave: true, + autoValidate: true, + allowParallelWork: true, // Standard: Paralleles Arbeiten erlaubt +} + +export const initialState: SDKState = { + // Metadata + version: '1.0.0', + projectVersion: 1, + lastModified: new Date(), + + // Tenant & User + tenantId: '', + userId: '', + subscription: 'PROFESSIONAL', + + // Project Context + projectId: '', + projectInfo: null, + + // Customer Type + customerType: null, + + // Company Profile + companyProfile: null, + + // Compliance Scope + complianceScope: null, + + // Source Policy + sourcePolicy: null, + + // Progress + currentPhase: 1, + currentStep: 'company-profile', + completedSteps: [], + checkpoints: {}, + + // Imported Documents (for existing customers) + importedDocuments: [], + gapAnalysis: null, + + // Phase 1 Data + useCases: [], + activeUseCase: null, + screening: null, + modules: [], + requirements: [], + controls: [], + evidence: [], + checklist: [], + risks: [], + + // Phase 2 Data + aiActClassification: null, + obligations: [], + dsfa: null, + toms: [], + retentionPolicies: [], + vvt: [], + documents: [], + cookieBanner: null, + consents: [], + dsrConfig: null, + escalationWorkflows: [], + + // IACE (Industrial AI Compliance Engine) + iaceProjects: [], + + // RAG Corpus Versioning + ragCorpusStatus: null, + + // Security + sbom: null, + securityIssues: [], + securityBacklog: [], + + // Catalog Manager + customCatalogs: {}, + + // UI State + commandBarHistory: [], + recentSearches: [], + preferences: initialPreferences, +} + +// ============================================================================= +// EXTENDED ACTION TYPES +// ============================================================================= + +// Extended action type to include demo data loading +export type ExtendedSDKAction = + | SDKAction + | { type: 'LOAD_DEMO_DATA'; payload: Partial } + +// ============================================================================= +// CONTEXT TYPES +// ============================================================================= + +export interface SDKContextValue { + state: SDKState + dispatch: React.Dispatch + + // Navigation + currentStep: SDKStep | undefined + goToStep: (stepId: string) => void + goToNextStep: () => void + goToPreviousStep: () => void + canGoNext: boolean + canGoPrevious: boolean + + // Progress + completionPercentage: number + phase1Completion: number + phase2Completion: number + packageCompletion: Record + + // Customer Type + setCustomerType: (type: CustomerType) => void + + // Company Profile + setCompanyProfile: (profile: CompanyProfile) => void + updateCompanyProfile: (updates: Partial) => void + + // Compliance Scope + setComplianceScope: (scope: import('./compliance-scope-types').ComplianceScopeState) => void + updateComplianceScope: (updates: Partial) => void + + // Import (for existing customers) + addImportedDocument: (doc: ImportedDocument) => void + setGapAnalysis: (analysis: GapAnalysis) => void + + // Checkpoints + validateCheckpoint: (checkpointId: string) => Promise + overrideCheckpoint: (checkpointId: string, reason: string) => Promise + getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined + + // State Updates + updateUseCase: (id: string, data: Partial) => void + addRisk: (risk: Risk) => void + updateControl: (id: string, data: Partial) => void + + // Persistence + saveState: () => Promise + loadState: () => Promise + + // Demo Data + loadDemoData: (demoState: Partial) => void + seedDemoData: () => Promise<{ success: boolean; message: string }> + clearDemoData: () => Promise + isDemoDataLoaded: boolean + + // Sync + syncState: SyncState + forceSyncToServer: () => Promise + isOnline: boolean + + // Export + exportState: (format: 'json' | 'pdf' | 'zip') => Promise + + // Command Bar + isCommandBarOpen: boolean + setCommandBarOpen: (open: boolean) => void + + // Project Management + projectId: string | undefined + createProject: (name: string, customerType: CustomerType, copyFromProjectId?: string) => Promise + listProjects: () => Promise + switchProject: (projectId: string) => void + archiveProject: (projectId: string) => Promise + restoreProject: (projectId: string) => Promise + permanentlyDeleteProject: (projectId: string) => Promise +} + +export const SDK_STORAGE_KEY = 'ai-compliance-sdk-state' diff --git a/admin-compliance/lib/sdk/context-validators.ts b/admin-compliance/lib/sdk/context-validators.ts new file mode 100644 index 0000000..e10f188 --- /dev/null +++ b/admin-compliance/lib/sdk/context-validators.ts @@ -0,0 +1,94 @@ +import { SDKState, CheckpointStatus } from './types' + +// ============================================================================= +// LOCAL CHECKPOINT VALIDATION +// ============================================================================= + +/** + * Performs local (client-side) checkpoint validation against the current SDK state. + * Returns a CheckpointStatus with errors/warnings populated. + */ +export function validateCheckpointLocally( + checkpointId: string, + state: SDKState +): CheckpointStatus { + const status: CheckpointStatus = { + checkpointId, + passed: true, + validatedAt: new Date(), + validatedBy: 'SYSTEM', + errors: [], + warnings: [], + } + + switch (checkpointId) { + case 'CP-PROF': + if (!state.companyProfile || !state.companyProfile.isComplete) { + status.passed = false + status.errors.push({ + ruleId: 'prof-complete', + field: 'companyProfile', + message: 'Unternehmensprofil muss vollständig ausgefüllt werden', + severity: 'ERROR', + }) + } + break + + case 'CP-UC': + if (state.useCases.length === 0) { + status.passed = false + status.errors.push({ + ruleId: 'uc-min-count', + field: 'useCases', + message: 'Mindestens ein Anwendungsfall muss erstellt werden', + severity: 'ERROR', + }) + } + break + + case 'CP-SCAN': + if (!state.screening || state.screening.status !== 'COMPLETED') { + status.passed = false + status.errors.push({ + ruleId: 'scan-complete', + field: 'screening', + message: 'Security Scan muss abgeschlossen sein', + severity: 'ERROR', + }) + } + break + + case 'CP-MOD': + if (state.modules.length === 0) { + status.passed = false + status.errors.push({ + ruleId: 'mod-min-count', + field: 'modules', + message: 'Mindestens ein Modul muss zugewiesen werden', + severity: 'ERROR', + }) + } + break + + case 'CP-RISK': { + const criticalRisks = state.risks.filter( + r => r.severity === 'CRITICAL' || r.severity === 'HIGH' + ) + const unmitigatedRisks = criticalRisks.filter( + r => r.mitigation.length === 0 + ) + if (unmitigatedRisks.length > 0) { + status.passed = false + status.errors.push({ + ruleId: 'critical-risks-mitigated', + field: 'risks', + message: `${unmitigatedRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`, + severity: 'ERROR', + }) + } + break + } + } + + return status +} diff --git a/admin-compliance/lib/sdk/context.tsx b/admin-compliance/lib/sdk/context.tsx index 08df34a..b0f1dd6 100644 --- a/admin-compliance/lib/sdk/context.tsx +++ b/admin-compliance/lib/sdk/context.tsx @@ -1,1280 +1,23 @@ 'use client' -import React, { createContext, useContext, useReducer, useEffect, useCallback, useMemo, useRef } from 'react' -import { useRouter, usePathname } from 'next/navigation' -import { - SDKState, - SDKAction, - SDKStep, - CheckpointStatus, - UseCaseAssessment, - Risk, - Control, - UserPreferences, - CustomerType, - CompanyProfile, - ImportedDocument, - GapAnalysis, - SDKPackageId, - ProjectInfo, - SDK_STEPS, - SDK_PACKAGES, - getStepById, - getStepByUrl, - getNextStep, - getPreviousStep, - getCompletionPercentage, - getPhaseCompletionPercentage, - getPackageCompletionPercentage, - getStepsForPackage, -} from './types' -import { exportToPDF, exportToZIP } from './export' -import { SDKApiClient, getSDKApiClient, resetSDKApiClient } from './api-client' -import { StateSyncManager, createStateSyncManager, SyncState } from './sync' -import { generateDemoState, seedDemoData as seedDemoDataApi, clearDemoData as clearDemoDataApi } from './demo-data' - -// ============================================================================= -// INITIAL STATE -// ============================================================================= - -const initialPreferences: UserPreferences = { - language: 'de', - theme: 'light', - compactMode: false, - showHints: true, - autoSave: true, - autoValidate: true, - allowParallelWork: true, // Standard: Paralleles Arbeiten erlaubt -} - -const initialState: SDKState = { - // Metadata - version: '1.0.0', - projectVersion: 1, - lastModified: new Date(), - - // Tenant & User - tenantId: '', - userId: '', - subscription: 'PROFESSIONAL', - - // Project Context - projectId: '', - projectInfo: null, - - // Customer Type - customerType: null, - - // Company Profile - companyProfile: null, - - // Compliance Scope - complianceScope: null, - - // Source Policy - sourcePolicy: null, - - // Progress - currentPhase: 1, - currentStep: 'company-profile', - completedSteps: [], - checkpoints: {}, - - // Imported Documents (for existing customers) - importedDocuments: [], - gapAnalysis: null, - - // Phase 1 Data - useCases: [], - activeUseCase: null, - screening: null, - modules: [], - requirements: [], - controls: [], - evidence: [], - checklist: [], - risks: [], - - // Phase 2 Data - aiActClassification: null, - obligations: [], - dsfa: null, - toms: [], - retentionPolicies: [], - vvt: [], - documents: [], - cookieBanner: null, - consents: [], - dsrConfig: null, - escalationWorkflows: [], - - // IACE (Industrial AI Compliance Engine) - iaceProjects: [], - - // RAG Corpus Versioning - ragCorpusStatus: null, - - // Security - sbom: null, - securityIssues: [], - securityBacklog: [], - - // Catalog Manager - customCatalogs: {}, - - // UI State - commandBarHistory: [], - recentSearches: [], - preferences: initialPreferences, -} - -// ============================================================================= -// EXTENDED ACTION TYPES -// ============================================================================= - -// Extended action type to include demo data loading -type ExtendedSDKAction = - | SDKAction - | { type: 'LOAD_DEMO_DATA'; payload: Partial } - -// ============================================================================= -// REDUCER -// ============================================================================= - -function sdkReducer(state: SDKState, action: ExtendedSDKAction): SDKState { - const updateState = (updates: Partial): SDKState => ({ - ...state, - ...updates, - lastModified: new Date(), - }) - - switch (action.type) { - case 'SET_STATE': - return updateState(action.payload) - - case 'LOAD_DEMO_DATA': - // Load demo data while preserving user preferences - return { - ...initialState, - ...action.payload, - tenantId: state.tenantId, - userId: state.userId, - preferences: state.preferences, - lastModified: new Date(), - } - - case 'SET_CURRENT_STEP': { - const step = getStepById(action.payload) - return updateState({ - currentStep: action.payload, - currentPhase: step?.phase || state.currentPhase, - }) - } - - case 'COMPLETE_STEP': - if (state.completedSteps.includes(action.payload)) { - return state - } - return updateState({ - completedSteps: [...state.completedSteps, action.payload], - }) - - case 'SET_CHECKPOINT_STATUS': - return updateState({ - checkpoints: { - ...state.checkpoints, - [action.payload.id]: action.payload.status, - }, - }) - - case 'SET_CUSTOMER_TYPE': - return updateState({ customerType: action.payload }) - - case 'SET_COMPANY_PROFILE': - return updateState({ companyProfile: action.payload }) - - case 'UPDATE_COMPANY_PROFILE': - return updateState({ - companyProfile: state.companyProfile - ? { ...state.companyProfile, ...action.payload } - : null, - }) - - case 'SET_COMPLIANCE_SCOPE': - return updateState({ complianceScope: action.payload }) - - case 'UPDATE_COMPLIANCE_SCOPE': - return updateState({ - complianceScope: state.complianceScope - ? { ...state.complianceScope, ...action.payload } - : null, - }) - - case 'ADD_IMPORTED_DOCUMENT': - return updateState({ - importedDocuments: [...state.importedDocuments, action.payload], - }) - - case 'UPDATE_IMPORTED_DOCUMENT': - return updateState({ - importedDocuments: state.importedDocuments.map(doc => - doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc - ), - }) - - case 'DELETE_IMPORTED_DOCUMENT': - return updateState({ - importedDocuments: state.importedDocuments.filter(doc => doc.id !== action.payload), - }) - - case 'SET_GAP_ANALYSIS': - return updateState({ gapAnalysis: action.payload }) - - case 'ADD_USE_CASE': - return updateState({ - useCases: [...state.useCases, action.payload], - }) - - case 'UPDATE_USE_CASE': - return updateState({ - useCases: state.useCases.map(uc => - uc.id === action.payload.id ? { ...uc, ...action.payload.data } : uc - ), - }) - - case 'DELETE_USE_CASE': - return updateState({ - useCases: state.useCases.filter(uc => uc.id !== action.payload), - activeUseCase: state.activeUseCase === action.payload ? null : state.activeUseCase, - }) - - case 'SET_ACTIVE_USE_CASE': - return updateState({ activeUseCase: action.payload }) - - case 'SET_SCREENING': - return updateState({ screening: action.payload }) - - case 'ADD_MODULE': - return updateState({ - modules: [...state.modules, action.payload], - }) - - case 'UPDATE_MODULE': - return updateState({ - modules: state.modules.map(m => - m.id === action.payload.id ? { ...m, ...action.payload.data } : m - ), - }) - - case 'ADD_REQUIREMENT': - return updateState({ - requirements: [...state.requirements, action.payload], - }) - - case 'UPDATE_REQUIREMENT': - return updateState({ - requirements: state.requirements.map(r => - r.id === action.payload.id ? { ...r, ...action.payload.data } : r - ), - }) - - case 'ADD_CONTROL': - return updateState({ - controls: [...state.controls, action.payload], - }) - - case 'UPDATE_CONTROL': - return updateState({ - controls: state.controls.map(c => - c.id === action.payload.id ? { ...c, ...action.payload.data } : c - ), - }) - - case 'ADD_EVIDENCE': - return updateState({ - evidence: [...state.evidence, action.payload], - }) - - case 'UPDATE_EVIDENCE': - return updateState({ - evidence: state.evidence.map(e => - e.id === action.payload.id ? { ...e, ...action.payload.data } : e - ), - }) - - case 'DELETE_EVIDENCE': - return updateState({ - evidence: state.evidence.filter(e => e.id !== action.payload), - }) - - case 'ADD_RISK': - return updateState({ - risks: [...state.risks, action.payload], - }) - - case 'UPDATE_RISK': - return updateState({ - risks: state.risks.map(r => - r.id === action.payload.id ? { ...r, ...action.payload.data } : r - ), - }) - - case 'DELETE_RISK': - return updateState({ - risks: state.risks.filter(r => r.id !== action.payload), - }) - - case 'SET_AI_ACT_RESULT': - return updateState({ aiActClassification: action.payload }) - - case 'ADD_OBLIGATION': - return updateState({ - obligations: [...state.obligations, action.payload], - }) - - case 'UPDATE_OBLIGATION': - return updateState({ - obligations: state.obligations.map(o => - o.id === action.payload.id ? { ...o, ...action.payload.data } : o - ), - }) - - case 'SET_DSFA': - return updateState({ dsfa: action.payload }) - - case 'ADD_TOM': - return updateState({ - toms: [...state.toms, action.payload], - }) - - case 'UPDATE_TOM': - return updateState({ - toms: state.toms.map(t => - t.id === action.payload.id ? { ...t, ...action.payload.data } : t - ), - }) - - case 'ADD_RETENTION_POLICY': - return updateState({ - retentionPolicies: [...state.retentionPolicies, action.payload], - }) - - case 'UPDATE_RETENTION_POLICY': - return updateState({ - retentionPolicies: state.retentionPolicies.map(p => - p.id === action.payload.id ? { ...p, ...action.payload.data } : p - ), - }) - - case 'ADD_PROCESSING_ACTIVITY': - return updateState({ - vvt: [...state.vvt, action.payload], - }) - - case 'UPDATE_PROCESSING_ACTIVITY': - return updateState({ - vvt: state.vvt.map(p => - p.id === action.payload.id ? { ...p, ...action.payload.data } : p - ), - }) - - case 'ADD_DOCUMENT': - return updateState({ - documents: [...state.documents, action.payload], - }) - - case 'UPDATE_DOCUMENT': - return updateState({ - documents: state.documents.map(d => - d.id === action.payload.id ? { ...d, ...action.payload.data } : d - ), - }) - - case 'SET_COOKIE_BANNER': - return updateState({ cookieBanner: action.payload }) - - case 'SET_DSR_CONFIG': - return updateState({ dsrConfig: action.payload }) - - case 'ADD_ESCALATION_WORKFLOW': - return updateState({ - escalationWorkflows: [...state.escalationWorkflows, action.payload], - }) - - case 'UPDATE_ESCALATION_WORKFLOW': - return updateState({ - escalationWorkflows: state.escalationWorkflows.map(w => - w.id === action.payload.id ? { ...w, ...action.payload.data } : w - ), - }) - - case 'ADD_SECURITY_ISSUE': - return updateState({ - securityIssues: [...state.securityIssues, action.payload], - }) - - case 'UPDATE_SECURITY_ISSUE': - return updateState({ - securityIssues: state.securityIssues.map(i => - i.id === action.payload.id ? { ...i, ...action.payload.data } : i - ), - }) - - case 'ADD_BACKLOG_ITEM': - return updateState({ - securityBacklog: [...state.securityBacklog, action.payload], - }) - - case 'UPDATE_BACKLOG_ITEM': - return updateState({ - securityBacklog: state.securityBacklog.map(i => - i.id === action.payload.id ? { ...i, ...action.payload.data } : i - ), - }) - - case 'ADD_COMMAND_HISTORY': - return updateState({ - commandBarHistory: [action.payload, ...state.commandBarHistory].slice(0, 50), - }) - - case 'SET_PREFERENCES': - return updateState({ - preferences: { ...state.preferences, ...action.payload }, - }) - - case 'ADD_CUSTOM_CATALOG_ENTRY': { - const entry = action.payload - const existing = state.customCatalogs[entry.catalogId] || [] - return updateState({ - customCatalogs: { - ...state.customCatalogs, - [entry.catalogId]: [...existing, entry], - }, - }) - } - - case 'UPDATE_CUSTOM_CATALOG_ENTRY': { - const { catalogId, entryId, data } = action.payload - const entries = state.customCatalogs[catalogId] || [] - return updateState({ - customCatalogs: { - ...state.customCatalogs, - [catalogId]: entries.map(e => - e.id === entryId ? { ...e, data: { ...e.data, ...data }, updatedAt: new Date().toISOString() } : e - ), - }, - }) - } - - case 'DELETE_CUSTOM_CATALOG_ENTRY': { - const { catalogId, entryId } = action.payload - const items = state.customCatalogs[catalogId] || [] - return updateState({ - customCatalogs: { - ...state.customCatalogs, - [catalogId]: items.filter(e => e.id !== entryId), - }, - }) - } - - case 'RESET_STATE': - return { ...initialState, lastModified: new Date() } - - default: - return state - } -} - -// ============================================================================= -// CONTEXT TYPES -// ============================================================================= - -interface SDKContextValue { - state: SDKState - dispatch: React.Dispatch - - // Navigation - currentStep: SDKStep | undefined - goToStep: (stepId: string) => void - goToNextStep: () => void - goToPreviousStep: () => void - canGoNext: boolean - canGoPrevious: boolean - - // Progress - completionPercentage: number - phase1Completion: number - phase2Completion: number - packageCompletion: Record - - // Customer Type - setCustomerType: (type: CustomerType) => void - - // Company Profile - setCompanyProfile: (profile: CompanyProfile) => void - updateCompanyProfile: (updates: Partial) => void - - // Compliance Scope - setComplianceScope: (scope: import('./compliance-scope-types').ComplianceScopeState) => void - updateComplianceScope: (updates: Partial) => void - - // Import (for existing customers) - addImportedDocument: (doc: ImportedDocument) => void - setGapAnalysis: (analysis: GapAnalysis) => void - - // Checkpoints - validateCheckpoint: (checkpointId: string) => Promise - overrideCheckpoint: (checkpointId: string, reason: string) => Promise - getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined - - // State Updates - updateUseCase: (id: string, data: Partial) => void - addRisk: (risk: Risk) => void - updateControl: (id: string, data: Partial) => void - - // Persistence - saveState: () => Promise - loadState: () => Promise - - // Demo Data - loadDemoData: (demoState: Partial) => void - seedDemoData: () => Promise<{ success: boolean; message: string }> - clearDemoData: () => Promise - isDemoDataLoaded: boolean - - // Sync - syncState: SyncState - forceSyncToServer: () => Promise - isOnline: boolean - - // Export - exportState: (format: 'json' | 'pdf' | 'zip') => Promise - - // Command Bar - isCommandBarOpen: boolean - setCommandBarOpen: (open: boolean) => void - - // Project Management - projectId: string | undefined - createProject: (name: string, customerType: CustomerType, copyFromProjectId?: string) => Promise - listProjects: () => Promise - switchProject: (projectId: string) => void - archiveProject: (projectId: string) => Promise - restoreProject: (projectId: string) => Promise - permanentlyDeleteProject: (projectId: string) => Promise -} - -const SDKContext = createContext(null) - -// ============================================================================= -// PROVIDER -// ============================================================================= - -const SDK_STORAGE_KEY = 'ai-compliance-sdk-state' - -interface SDKProviderProps { - children: React.ReactNode - tenantId?: string - userId?: string - projectId?: string - enableBackendSync?: boolean -} - -export function SDKProvider({ - children, - tenantId = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e', - userId = 'default', - projectId, - enableBackendSync = false, -}: SDKProviderProps) { - const router = useRouter() - const pathname = usePathname() - const [state, dispatch] = useReducer(sdkReducer, { - ...initialState, - tenantId, - userId, - projectId: projectId || '', - }) - const [isCommandBarOpen, setCommandBarOpen] = React.useState(false) - const [isInitialized, setIsInitialized] = React.useState(false) - const [syncState, setSyncState] = React.useState({ - status: 'idle', - lastSyncedAt: null, - localVersion: 0, - serverVersion: 0, - pendingChanges: 0, - error: null, - }) - const [isOnline, setIsOnline] = React.useState(true) - - // Refs for sync manager and API client - const apiClientRef = useRef(null) - const syncManagerRef = useRef(null) - - // Initialize API client and sync manager - useEffect(() => { - if (enableBackendSync && typeof window !== 'undefined') { - apiClientRef.current = getSDKApiClient(tenantId, projectId) - - syncManagerRef.current = createStateSyncManager( - apiClientRef.current, - tenantId, - { - debounceMs: 2000, - maxRetries: 3, - }, - { - onSyncStart: () => { - setSyncState(prev => ({ ...prev, status: 'syncing' })) - }, - onSyncComplete: (syncedState) => { - setSyncState(prev => ({ - ...prev, - status: 'idle', - lastSyncedAt: new Date(), - pendingChanges: 0, - })) - // Update state if it differs from current - if (syncedState.lastModified > state.lastModified) { - dispatch({ type: 'SET_STATE', payload: syncedState }) - } - }, - onSyncError: (error) => { - setSyncState(prev => ({ - ...prev, - status: 'error', - error: error.message, - })) - }, - onConflict: () => { - setSyncState(prev => ({ ...prev, status: 'conflict' })) - }, - onOffline: () => { - setIsOnline(false) - setSyncState(prev => ({ ...prev, status: 'offline' })) - }, - onOnline: () => { - setIsOnline(true) - setSyncState(prev => ({ ...prev, status: 'idle' })) - }, - }, - projectId - ) - } - - return () => { - if (syncManagerRef.current) { - syncManagerRef.current.destroy() - syncManagerRef.current = null - } - if (enableBackendSync) { - resetSDKApiClient() - apiClientRef.current = null - } - } - }, [enableBackendSync, tenantId, projectId]) - - // Sync current step with URL - useEffect(() => { - if (pathname) { - const step = getStepByUrl(pathname) - if (step && step.id !== state.currentStep) { - dispatch({ type: 'SET_CURRENT_STEP', payload: step.id }) - } - } - }, [pathname, state.currentStep]) - - // Storage key — per tenant+project - const storageKey = projectId - ? `${SDK_STORAGE_KEY}-${tenantId}-${projectId}` - : `${SDK_STORAGE_KEY}-${tenantId}` - - // Load state on mount (localStorage first, then server) - useEffect(() => { - const loadInitialState = async () => { - try { - // First, try loading from localStorage - const stored = localStorage.getItem(storageKey) - if (stored) { - const parsed = JSON.parse(stored) - if (parsed.lastModified) { - parsed.lastModified = new Date(parsed.lastModified) - } - dispatch({ type: 'SET_STATE', payload: parsed }) - } - - // Then, try loading from server if backend sync is enabled - if (enableBackendSync && syncManagerRef.current) { - const serverState = await syncManagerRef.current.loadFromServer() - if (serverState) { - // Server state is newer, use it - const localTime = stored ? new Date(JSON.parse(stored).lastModified).getTime() : 0 - const serverTime = new Date(serverState.lastModified).getTime() - if (serverTime > localTime) { - dispatch({ type: 'SET_STATE', payload: serverState }) - } - } - } - - // Load project metadata (name, status, etc.) from backend - if (enableBackendSync && projectId && apiClientRef.current) { - try { - const info = await apiClientRef.current.getProject(projectId) - dispatch({ type: 'SET_STATE', payload: { projectInfo: info } }) - } catch (err) { - console.warn('Failed to load project info:', err) - } - } - } catch (error) { - console.error('Failed to load SDK state:', error) - } - setIsInitialized(true) - } - - loadInitialState() - }, [tenantId, projectId, enableBackendSync, storageKey]) - - // Auto-save to localStorage and sync to server - useEffect(() => { - if (!isInitialized || !state.preferences.autoSave) return - - const saveTimeout = setTimeout(() => { - try { - // Save to localStorage (per tenant+project) - localStorage.setItem(storageKey, JSON.stringify(state)) - - // Sync to server if backend sync is enabled - if (enableBackendSync && syncManagerRef.current) { - syncManagerRef.current.queueSync(state) - } - } catch (error) { - console.error('Failed to save SDK state:', error) - } - }, 1000) - - return () => clearTimeout(saveTimeout) - }, [state, tenantId, projectId, isInitialized, enableBackendSync, storageKey]) - - // Keyboard shortcut for Command Bar - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault() - setCommandBarOpen(prev => !prev) - } - if (e.key === 'Escape' && isCommandBarOpen) { - setCommandBarOpen(false) - } - } - - window.addEventListener('keydown', handleKeyDown) - return () => window.removeEventListener('keydown', handleKeyDown) - }, [isCommandBarOpen]) - - // Navigation - const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep]) - - const goToStep = useCallback( - (stepId: string) => { - const step = getStepById(stepId) - if (step) { - dispatch({ type: 'SET_CURRENT_STEP', payload: stepId }) - const url = projectId ? `${step.url}?project=${projectId}` : step.url - router.push(url) - } - }, - [router, projectId] - ) - - const goToNextStep = useCallback(() => { - const nextStep = getNextStep(state.currentStep, state) - if (nextStep) { - goToStep(nextStep.id) - } - }, [state, goToStep]) - - const goToPreviousStep = useCallback(() => { - const prevStep = getPreviousStep(state.currentStep, state) - if (prevStep) { - goToStep(prevStep.id) - } - }, [state, goToStep]) - - const canGoNext = useMemo(() => { - return getNextStep(state.currentStep, state) !== undefined - }, [state]) - - const canGoPrevious = useMemo(() => { - return getPreviousStep(state.currentStep, state) !== undefined - }, [state]) - - // Progress - const completionPercentage = useMemo(() => getCompletionPercentage(state), [state]) - const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state]) - const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state]) - - // Package Completion - const packageCompletion = useMemo(() => { - const completion: Record = { - 'vorbereitung': getPackageCompletionPercentage(state, 'vorbereitung'), - 'analyse': getPackageCompletionPercentage(state, 'analyse'), - 'dokumentation': getPackageCompletionPercentage(state, 'dokumentation'), - 'rechtliche-texte': getPackageCompletionPercentage(state, 'rechtliche-texte'), - 'betrieb': getPackageCompletionPercentage(state, 'betrieb'), - } - return completion - }, [state]) - - // Customer Type - const setCustomerType = useCallback((type: CustomerType) => { - dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type }) - }, []) - - // Company Profile - const setCompanyProfile = useCallback((profile: CompanyProfile) => { - dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile }) - }, []) - - const updateCompanyProfile = useCallback((updates: Partial) => { - dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates }) - }, []) - - // Compliance Scope - const setComplianceScope = useCallback((scope: import('./compliance-scope-types').ComplianceScopeState) => { - dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scope }) - }, []) - - const updateComplianceScope = useCallback((updates: Partial) => { - dispatch({ type: 'UPDATE_COMPLIANCE_SCOPE', payload: updates }) - }, []) - - // Import Document - const addImportedDocument = useCallback((doc: ImportedDocument) => { - dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc }) - }, []) - - // Gap Analysis - const setGapAnalysis = useCallback((analysis: GapAnalysis) => { - dispatch({ type: 'SET_GAP_ANALYSIS', payload: analysis }) - }, []) - - // Checkpoints - const validateCheckpoint = useCallback( - async (checkpointId: string): Promise => { - // Try backend validation if available - if (enableBackendSync && apiClientRef.current) { - try { - const result = await apiClientRef.current.validateCheckpoint(checkpointId, state) - const status: CheckpointStatus = { - checkpointId: result.checkpointId, - passed: result.passed, - validatedAt: new Date(result.validatedAt), - validatedBy: result.validatedBy, - errors: result.errors, - warnings: result.warnings, - } - dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } }) - return status - } catch { - // Fall back to local validation - } - } - - // Local validation - const status: CheckpointStatus = { - checkpointId, - passed: true, - validatedAt: new Date(), - validatedBy: 'SYSTEM', - errors: [], - warnings: [], - } - - switch (checkpointId) { - case 'CP-PROF': - if (!state.companyProfile || !state.companyProfile.isComplete) { - status.passed = false - status.errors.push({ - ruleId: 'prof-complete', - field: 'companyProfile', - message: 'Unternehmensprofil muss vollständig ausgefüllt werden', - severity: 'ERROR', - }) - } - break - - case 'CP-UC': - if (state.useCases.length === 0) { - status.passed = false - status.errors.push({ - ruleId: 'uc-min-count', - field: 'useCases', - message: 'Mindestens ein Anwendungsfall muss erstellt werden', - severity: 'ERROR', - }) - } - break - - case 'CP-SCAN': - if (!state.screening || state.screening.status !== 'COMPLETED') { - status.passed = false - status.errors.push({ - ruleId: 'scan-complete', - field: 'screening', - message: 'Security Scan muss abgeschlossen sein', - severity: 'ERROR', - }) - } - break - - case 'CP-MOD': - if (state.modules.length === 0) { - status.passed = false - status.errors.push({ - ruleId: 'mod-min-count', - field: 'modules', - message: 'Mindestens ein Modul muss zugewiesen werden', - severity: 'ERROR', - }) - } - break - - case 'CP-RISK': - const criticalRisks = state.risks.filter( - r => r.severity === 'CRITICAL' || r.severity === 'HIGH' - ) - const unmitigatedRisks = criticalRisks.filter( - r => r.mitigation.length === 0 - ) - if (unmitigatedRisks.length > 0) { - status.passed = false - status.errors.push({ - ruleId: 'critical-risks-mitigated', - field: 'risks', - message: `${unmitigatedRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`, - severity: 'ERROR', - }) - } - break - } - - dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } }) - return status - }, - [state, enableBackendSync] - ) - - const overrideCheckpoint = useCallback( - async (checkpointId: string, reason: string): Promise => { - const existingStatus = state.checkpoints[checkpointId] - const overriddenStatus: CheckpointStatus = { - ...existingStatus, - checkpointId, - passed: true, - overrideReason: reason, - overriddenBy: state.userId, - overriddenAt: new Date(), - errors: [], - warnings: existingStatus?.warnings || [], - } - - dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overriddenStatus } }) - }, - [state.checkpoints, state.userId] - ) - - const getCheckpointStatus = useCallback( - (checkpointId: string): CheckpointStatus | undefined => { - return state.checkpoints[checkpointId] - }, - [state.checkpoints] - ) - - // State Updates - const updateUseCase = useCallback( - (id: string, data: Partial) => { - dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } }) - }, - [] - ) - - const addRisk = useCallback((risk: Risk) => { - dispatch({ type: 'ADD_RISK', payload: risk }) - }, []) - - const updateControl = useCallback( - (id: string, data: Partial) => { - dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } }) - }, - [] - ) - - // Demo Data Loading - const loadDemoData = useCallback((demoState: Partial) => { - dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState }) - }, []) - - // Seed demo data via API (stores like real customer data) - const seedDemoData = useCallback(async (): Promise<{ success: boolean; message: string }> => { - try { - // Generate demo state - const demoState = generateDemoState(tenantId, userId) as SDKState - - // Save via API (same path as real customer data) - if (enableBackendSync && apiClientRef.current) { - await apiClientRef.current.saveState(demoState) - } - - // Also save to localStorage for immediate availability - localStorage.setItem(storageKey, JSON.stringify(demoState)) - - // Update local state - dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState }) - - return { success: true, message: `Demo-Daten erfolgreich geladen für Tenant ${tenantId}` } - } catch (error) { - console.error('Failed to seed demo data:', error) - return { - success: false, - message: error instanceof Error ? error.message : 'Unbekannter Fehler beim Laden der Demo-Daten', - } - } - }, [tenantId, userId, enableBackendSync, storageKey]) - - // Clear demo data - const clearDemoData = useCallback(async (): Promise => { - try { - // Delete from API - if (enableBackendSync && apiClientRef.current) { - await apiClientRef.current.deleteState() - } - - // Clear localStorage - localStorage.removeItem(storageKey) - - // Reset local state - dispatch({ type: 'RESET_STATE' }) - - return true - } catch (error) { - console.error('Failed to clear demo data:', error) - return false - } - }, [storageKey, enableBackendSync]) - - // Check if demo data is loaded (has use cases with demo- prefix) - const isDemoDataLoaded = useMemo(() => { - return state.useCases.length > 0 && state.useCases.some(uc => uc.id.startsWith('demo-')) - }, [state.useCases]) - - // Persistence - const saveState = useCallback(async (): Promise => { - try { - localStorage.setItem(storageKey, JSON.stringify(state)) - - if (enableBackendSync && syncManagerRef.current) { - await syncManagerRef.current.forcSync(state) - } - } catch (error) { - console.error('Failed to save SDK state:', error) - throw error - } - }, [state, storageKey, enableBackendSync]) - - const loadState = useCallback(async (): Promise => { - try { - if (enableBackendSync && syncManagerRef.current) { - const serverState = await syncManagerRef.current.loadFromServer() - if (serverState) { - dispatch({ type: 'SET_STATE', payload: serverState }) - return - } - } - - // Fall back to localStorage - const stored = localStorage.getItem(storageKey) - if (stored) { - const parsed = JSON.parse(stored) - dispatch({ type: 'SET_STATE', payload: parsed }) - } - } catch (error) { - console.error('Failed to load SDK state:', error) - throw error - } - }, [storageKey, enableBackendSync]) - - // Force sync to server - const forceSyncToServer = useCallback(async (): Promise => { - if (enableBackendSync && syncManagerRef.current) { - await syncManagerRef.current.forcSync(state) - } - }, [state, enableBackendSync]) - - // Project Management - const createProject = useCallback( - async (name: string, customerType: CustomerType, copyFromProjectId?: string): Promise => { - if (!apiClientRef.current && enableBackendSync) { - apiClientRef.current = getSDKApiClient(tenantId, projectId) - } - if (!apiClientRef.current) { - throw new Error('Backend sync not enabled') - } - return apiClientRef.current.createProject({ - name, - customer_type: customerType, - copy_from_project_id: copyFromProjectId, - }) - }, - [enableBackendSync, tenantId, projectId] - ) - - const listProjectsFn = useCallback(async (): Promise => { - // Ensure API client exists (may not be set yet if useEffect hasn't fired) - if (!apiClientRef.current && enableBackendSync) { - apiClientRef.current = getSDKApiClient(tenantId, projectId) - } - if (!apiClientRef.current) { - return [] - } - const result = await apiClientRef.current.listProjects() - return result.projects - }, [enableBackendSync, tenantId, projectId]) - - const switchProject = useCallback( - (newProjectId: string) => { - // Navigate to the SDK dashboard with the new project - const params = new URLSearchParams(window.location.search) - params.set('project', newProjectId) - router.push(`/sdk?${params.toString()}`) - }, - [router] - ) - - const archiveProjectFn = useCallback( - async (archiveId: string): Promise => { - if (!apiClientRef.current && enableBackendSync) { - apiClientRef.current = getSDKApiClient(tenantId, projectId) - } - if (!apiClientRef.current) { - throw new Error('Backend sync not enabled') - } - await apiClientRef.current.archiveProject(archiveId) - }, - [enableBackendSync, tenantId, projectId] - ) - - const restoreProjectFn = useCallback( - async (restoreId: string): Promise => { - if (!apiClientRef.current && enableBackendSync) { - apiClientRef.current = getSDKApiClient(tenantId, projectId) - } - if (!apiClientRef.current) { - throw new Error('Backend sync not enabled') - } - return apiClientRef.current.restoreProject(restoreId) - }, - [enableBackendSync, tenantId, projectId] - ) - - const permanentlyDeleteProjectFn = useCallback( - async (deleteId: string): Promise => { - if (!apiClientRef.current && enableBackendSync) { - apiClientRef.current = getSDKApiClient(tenantId, projectId) - } - if (!apiClientRef.current) { - throw new Error('Backend sync not enabled') - } - await apiClientRef.current.permanentlyDeleteProject(deleteId) - }, - [enableBackendSync, tenantId, projectId] - ) - - // Export - const exportState = useCallback( - async (format: 'json' | 'pdf' | 'zip'): Promise => { - switch (format) { - case 'json': - return new Blob([JSON.stringify(state, null, 2)], { - type: 'application/json', - }) - - case 'pdf': - return exportToPDF(state) - - case 'zip': - return exportToZIP(state) - - default: - throw new Error(`Unknown export format: ${format}`) - } - }, - [state] - ) - - const value: SDKContextValue = { - state, - dispatch, - currentStep, - goToStep, - goToNextStep, - goToPreviousStep, - canGoNext, - canGoPrevious, - completionPercentage, - phase1Completion, - phase2Completion, - packageCompletion, - setCustomerType, - setCompanyProfile, - updateCompanyProfile, - setComplianceScope, - updateComplianceScope, - addImportedDocument, - setGapAnalysis, - validateCheckpoint, - overrideCheckpoint, - getCheckpointStatus, - updateUseCase, - addRisk, - updateControl, - saveState, - loadState, - loadDemoData, - seedDemoData, - clearDemoData, - isDemoDataLoaded, - syncState, - forceSyncToServer, - isOnline, - exportState, - isCommandBarOpen, - setCommandBarOpen, - projectId, - createProject, - listProjects: listProjectsFn, - switchProject, - archiveProject: archiveProjectFn, - restoreProject: restoreProjectFn, - permanentlyDeleteProject: permanentlyDeleteProjectFn, - } - - return {children} -} - -// ============================================================================= -// HOOK -// ============================================================================= - -export function useSDK(): SDKContextValue { - const context = useContext(SDKContext) - if (!context) { - throw new Error('useSDK must be used within SDKProvider') - } - return context -} - -// ============================================================================= -// EXPORTS -// ============================================================================= - -export { SDKContext, initialState } +/** + * AI Compliance SDK Context — barrel re-export. + * + * The implementation has been split into: + * - context-types.ts — SDKContextValue interface, initialState, ExtendedSDKAction + * - context-reducer.ts — sdkReducer + * - context-provider.tsx — SDKProvider component + SDKContext + * - context-hooks.ts — useSDK hook + * + * All public symbols are re-exported here so that existing imports + * (e.g. `import { useSDK } from '@/lib/sdk/context'`) continue to work. + */ + +export { initialState, SDK_STORAGE_KEY } from './context-types' +export type { SDKContextValue, ExtendedSDKAction } from './context-types' + +export { sdkReducer } from './context-reducer' + +export { SDKContext, SDKProvider } from './context-provider' + +export { useSDK } from './context-hooks'